<?php

/**
 * The class which handles Frontend CSS.
 *
 * @author     Time.ly Network Inc.
 * @since      2.0
 *
 * @package    AI1EC
 * @subpackage AI1EC.Css
 */
class Ai1ec_Css_Frontend extends Ai1ec_Base {

	const QUERY_STRING_PARAM                = 'ai1ec_render_css';

	// This is for testing purpose, set it to AI1EC_PARSE_LESS_FILES_AT_EVERY_REQUEST value.
	const PARSE_LESS_FILES_AT_EVERY_REQUEST = AI1EC_PARSE_LESS_FILES_AT_EVERY_REQUEST;

	const KEY_FOR_PERSISTANCE               = 'ai1ec_parsed_css';
	/**
	 * @var Ai1ec_Persistence_Context
	 */
	private $persistance_context;

	/**
	 * @var Ai1ec_Less_Lessphp
	 */
	private $lessphp_controller;

	/**
	 * @var Ai1ec_Option
	 */
	private $db_adapter;

	/**
	 * @var Ai1ec_Template_Adapter
	 */
	private $template_adapter;

	/**
	 * Possible paths/url for file cache
	 *
	 * @var array
	 */
	protected $_cache_paths = array();

	/**
	 * @var array which have been checked and are not writable
	 */
	protected $_folders_not_writable = array();

	public function __construct(
		Ai1ec_Registry_Object $registry
	) {
		parent::__construct( $registry );
		$this->_cache_paths[] = array(
			'path' => AI1EC_CACHE_PATH,
			'url'  => AI1EC_CACHE_URL
		);
		if ( apply_filters( 'ai1ec_check_static_dir', true ) ) {
			$filesystem = $this->_registry->get( 'filesystem.checker' );
			$wp_static_folder = $filesystem->get_ai1ec_static_dir_if_available();
			if ( '' !== $wp_static_folder ) {
				$this->_cache_paths[] = array(
					'path' => $wp_static_folder,
					'url'  => content_url() . '/uploads/ai1ec_static/'
				);
			}
		}
		$this->persistance_context = $this->_registry->get(
			'cache.strategy.persistence-context',
			self::KEY_FOR_PERSISTANCE,
			$this->_cache_paths,
			true
		);
		if ( ! $this->persistance_context->is_file_cache() ) {
			 /* @TODO: move this to Settings -> Advanced -> Cache */
		}
		$this->lessphp_controller  = $this->_registry->get( 'less.lessphp' );
		$this->db_adapter          = $this->_registry->get( 'model.option' );
	}

	/**
	 *
	 * Get if file cache is enabled
	 * @return boolean
	 */
	public function is_file_cache_enabled() {
		return $this->persistance_context->is_file_cache();
	}

	/**
	 * Get folders which are not writable
	 *
	 * @return array
	 */
	public function get_folders_not_writable() {
		return $this->_folders_not_writable;
	}
	/**
	 * Renders the css for our frontend.
	 *
	 * Sets etags to avoid sending not needed data
	 */
	public function render_css() {
		header( 'HTTP/1.1 200 OK' );
		header( 'Content-Type: text/css', true, 200 );
		// Aggressive caching to save future requests from the same client.
		$etag = '"' . md5( __FILE__ . $_GET[self::QUERY_STRING_PARAM] ) . '"';
		header( 'ETag: ' . $etag );
		$max_age = 31536000;
		$time_sys = $this->_registry->get( 'date.system' );
		header(
			'Expires: ' .
			gmdate(
				'D, d M Y H:i:s',
				$time_sys->current_time() + $max_age
			) .
			' GMT'
		);
		header( 'Cache-Control: public, max-age=' . $max_age );
		if (
			empty( $_SERVER['HTTP_IF_NONE_MATCH'] ) ||
			$etag !== stripslashes( $_SERVER['HTTP_IF_NONE_MATCH'] )
		) {
			// compress data if possible
			$this->_registry->get( 'compatibility.ob' )
				->gzip_if_possible( $this->get_compiled_css() );
		} else {
			// Not modified!
			status_header( 304 );
		}
		// We're done!
		Ai1ec_Http_Response_Helper::stop( 0 );
	}

	/**
	 *
	 * @param string $css
	 * @throws Ai1ec_Cache_Write_Exception
	 */
	public function update_persistence_layer( $css ) {
		$filename = $this->persistance_context->write_data_to_persistence( $css );
		$this->db_adapter->set(
			'ai1ec_filename_css',
			isset( $filename['file'] ) ? $filename['file'] : false,
			true
		);
		$this->save_less_parse_time( $filename['url'] );
	}


	/**
	 * Get the url to retrieve the css
	 *
	 * @return string
	 */
	public function get_css_url() {
		// get what's saved. I t could be false, a int or a string.
		// if it's false or a int, use PHP to render CSS
		$saved_par = $this->db_adapter->get( self::QUERY_STRING_PARAM );
		// if it's empty it's a new install probably. Return static css.
		// if it's numeric, just consider it a new install
		if ( empty( $saved_par ) ) {
			$theme = $this->_registry->get(
				'model.option'
			)->get( 'ai1ec_current_theme' );
			return Ai1ec_Http_Response_Helper::remove_protocols(
				apply_filters(
					'ai1ec_frontend_standard_css_url',
					$theme['theme_url'] . '/css/ai1ec_parsed_css.css'
				)
			);
		}
		if ( is_numeric( $saved_par ) ) {
			if ( $this->_registry->get( 'model.settings' )->get( 'render_css_as_link' ) ) {
				$time = (int) $saved_par;
				$template_helper = $this->_registry->get( 'template.link.helper' );
				return Ai1ec_Http_Response_Helper::remove_protocols(
					add_query_arg(
						array( self::QUERY_STRING_PARAM => $time, ),
						trailingslashit( ai1ec_get_site_url() )
					)
				);
			} else {
				add_action( 'wp_head', array( $this, 'echo_css' ) );
				return '';
			}

		}
		// otherwise return the string
		return Ai1ec_Http_Response_Helper::remove_protocols(
			$saved_par
		);
	}

	/**
	 * Create the link that will be added to the frontend
	 */
	public function add_link_to_html_for_frontend() {
		$url = $this->get_css_url();
		if ( '' !== $url && ! is_admin() ) {
			wp_enqueue_style( 'ai1ec_style', $url, array(), AI1EC_VERSION );
		}
	}

	public function echo_css() {
		echo '<style>';
		echo $this->get_compiled_css();
		echo '</style>';
	}

	/**
	 * Invalidate the persistence layer only after a successful compile of the
	 * LESS files.
	 *
	 * @param  array   $variables          LESS variable array to use
	 * @param  boolean $update_persistence Whether the persist successful compile
	 *
	 * @return boolean                     Whether successful
	 */
	public function invalidate_cache(
		array $variables    = null,
		$update_persistence = false
	) {
		if ( ! $this->lessphp_controller->is_compilation_needed( $variables ) ) {
			$this->_registry->get(
				'model.option'
			)->delete( 'ai1ec_render_css' );
			return true;
		}
		$notification = $this->_registry->get( 'notification.admin' );
		if (
			! $this->_registry->get(
				'compatibility.memory'
			)->check_available_memory( AI1EC_LESS_MIN_AVAIL_MEMORY )
		) {
			$message = sprintf(
				Ai1ec_I18n::__(
					'CSS compilation failed because you don\'t have enough free memory (a minimum of %s is needed). Your calendar will not render or function properly without CSS. Please read <a href="http://time.ly/document/user-guide/getting-started/pre-sale-questions/">this article</a> to learn how to increase your PHP memory limit.'
				),
				AI1EC_LESS_MIN_AVAIL_MEMORY
			);
			$notification->store(
				$message,
				'error',
				1,
				array( Ai1ec_Notification_Admin::RCPT_ADMIN ),
				true
			);
			return;
		}
		try {
			// Try to parse the css
			$css = $this->lessphp_controller->parse_less_files( $variables );
			// Reset the parse time to force a browser reload of the CSS, whether we are
			// updating persistence or not. Do it here to be sure files compile ok.
			$this->save_less_parse_time();
			if ( $update_persistence ) {
				$this->update_persistence_layer( $css );
			} else {
				$this->persistance_context->delete_data_from_persistence();
			}
		} catch ( Ai1ec_Cache_Write_Exception $e ) {
			// This means successful during parsing but problems persisting the CSS.
			$message = '<p>' . Ai1ec_I18n::__( "The LESS file compiled correctly but there was an error while saving the generated CSS to persistence." ) . '</p>';
			$notification->store( $message, 'error' );
			return false;
		} catch ( Exception $e ) {
			// An error from lessphp.
			$message = sprintf(
				Ai1ec_I18n::__( '<p><strong>There was an error while compiling CSS.</strong> The message returned was: <em>%s</em></p>' ),
				$e->getMessage()
			);
			$notification->store( $message, 'error', 1 );
			return false;
		}
		return true;
	}


	/**
	 * Update the less variables on the DB and recompile the CSS
	 *
	 * @param array $variables
	 * @param boolean $resetting are we resetting or updating variables?
	 */
	public function update_variables_and_compile_css( array $variables, $resetting ) {
		$no_parse_errors = $this->invalidate_cache( $variables, true );
		$notification    = $this->_registry->get( 'notification.admin' );

		if ( $no_parse_errors ) {
			$this->db_adapter->set(
				Ai1ec_Less_Lessphp::DB_KEY_FOR_LESS_VARIABLES,
				$variables
			);

			if ( true === $resetting ) {
				$message = sprintf(
					'<p>' . Ai1ec_I18n::__(
						"Theme options were successfully reset to their default values. <a href='%s'>Visit site</a>"
					) . '</p>',
					ai1ec_get_site_url()
				);
			} else {
				$message = sprintf(
					'<p>' .Ai1ec_I18n::__(
						"Theme options were updated successfully. <a href='%s'>Visit site</a>"
					) . '</p>',
					ai1ec_get_site_url()
				);
			}

			$notification->store( $message );
		}
	}
	/**
	 * Try to get the CSS from cache.
	 * If it's not there re-generate it and save it to cache
	 * If we are in preview mode, recompile the css using the theme present in the url.
	 *
	 */
	public function get_compiled_css() {
		try {
			// If we want to force a recompile, we throw an exception.
			if( self::PARSE_LESS_FILES_AT_EVERY_REQUEST === true ) {
				throw new Ai1ec_Cache_Not_Set_Exception();
			}else {
				// This throws an exception if the key is not set
				$css = $this->persistance_context->get_data_from_persistence();
				return $css;
			}
		} catch ( Ai1ec_Cache_Not_Set_Exception $e ) {
			$css = $this->lessphp_controller->parse_less_files();
			try {
				$this->update_persistence_layer( $css );
				return $css;
			} catch ( Ai1ec_Cache_Write_Exception $e ) {
				if ( ! self::PARSE_LESS_FILES_AT_EVERY_REQUEST ) {
					$this->_registry->get( 'notification.admin' )
						->store(
							sprintf(
								__(
									'Your CSS is being compiled on every request, which causes your calendar to perform slowly. The following error occurred: %s',
									AI1EC_PLUGIN_NAME
								),
								$e->getMessage()
							),
							'error',
							2,
							array( Ai1ec_Notification_Admin::RCPT_ADMIN ),
							true
						);
				}

				// If something is really broken, still return the css.
				// This means we parse it every time. This should never happen.
				return $css;
			}
		}
	}

	/**
	 * Save the path to the CSS file or false to load standard CSS
	 */
	private function save_less_parse_time( $data = false ) {
		$to_save = is_string( $data ) ?
					$data :
					$this->_registry->get( 'date.system' )->current_time();
		$this->db_adapter->set(
			self::QUERY_STRING_PARAM,
			$to_save,
			true
		);
	}
}
