<?php
/**
 * @package Habari
 *
 */

/**
 * Habari Themes class
 *
 */
class Themes
{
	private static $all_themes = null;
	private static $all_data = null;

	/**
	 * Returns the theme dir and path information
	 * @return array An array of Theme data
	 **/
	public static function get_all()
	{
		if ( !isset( self::$all_themes ) ) {
			$dirs = array( HABARI_PATH . '/system/themes/*' , HABARI_PATH . '/3rdparty/themes/*', HABARI_PATH . '/user/themes/*' );
			if ( Site::is( 'multi' ) ) {
				$dirs[] = Site::get_dir( 'config' ) . '/themes/*';
			}
			$themes = array();
			foreach ( $dirs as $dir ) {
				$themes = array_merge( $themes, Utils::glob( $dir, GLOB_ONLYDIR | GLOB_MARK ) );
			}

			$themes = array_filter( $themes, create_function( '$a', 'return file_exists( $a . "/theme.xml" );' ) );
			$themefiles = array_map( 'basename', $themes );
			self::$all_themes = array_combine( $themefiles, $themes );
		}
		return self::$all_themes;
	}

	/**
	 * Returns all theme information -- dir, path, theme.xml, screenshot url
	 * @return array An array of Theme data
	 **/
	public static function get_all_data()
	{
		if ( !isset( self::$all_data ) ) {
			foreach ( self::get_all() as $theme_dir => $theme_path ) {
				$themedata = array();
				$themedata['dir'] = $theme_dir;
				$themedata['path'] = $theme_path;
				$themedata['theme_dir'] = $theme_path;

				$themedata['info'] = simplexml_load_file( $theme_path . '/theme.xml' );
				if ( $themedata['info']->getName() != 'pluggable' || (string) $themedata['info']->attributes()->type != 'theme' ) {
					$themedata['screenshot'] = Site::get_url( 'admin_theme' ) . "/images/screenshot_default.png";
					$themedata['info']->description = '<span class="error">' . _t( 'This theme is a legacy theme that is not compatible with Habari ' ) . Version::get_habariversion() . '. <br><br>Please update your theme.</span>';
					$themedata['info']->license = '';
				}
				else {
					foreach ( $themedata['info'] as $name=>$value ) {
						$themedata[$name] = (string) $value;
					}

					if ( $screenshot = Utils::glob( $theme_path . '/screenshot.{png,jpg,gif}', GLOB_BRACE ) ) {
						$themedata['screenshot'] = Site::get_url( 'habari' ) . dirname( str_replace( HABARI_PATH, '', $theme_path ) ) . '/' . basename( $theme_path ) . "/" . basename( reset( $screenshot ) );
					}
					else {
						$themedata['screenshot'] = Site::get_url( 'admin_theme' ) . "/images/screenshot_default.png";
					}
				}
				
				self::$all_data[$theme_dir] = $themedata;
			}
		}
		return self::$all_data;
	}
	
	/**
	 * Returns the name of the active or previewed theme
	 * 
	 * @params boolean $nopreview If true, return the real active theme, not the preview
	 * @return string the current theme or previewed theme's directory name
	 */
	public static function get_theme_dir( $nopreview = false )
	{
		if ( !$nopreview && isset( $_SESSION['user_theme_dir'] ) ) {
			$theme_dir = $_SESSION['user_theme_dir'];
		}
		else {
			$theme_dir = Options::get( 'theme_dir' );
		}
		$theme_dir = Plugins::filter( 'get_theme_dir', $theme_dir );
		return $theme_dir;
	}

	/**
	 * Returns the active theme's full directory path.
	 * @params boolean $nopreview If true, return the real active theme, not the preview
	 * @return string The full path to the active theme directory
	 */
	private static function get_active_theme_dir( $nopreview = false )
	{
		$theme_dir = self::get_theme_dir( $nopreview );
		$themes = Themes::get_all();

		// If our active theme directory has gone missing, iterate through the others until we find one we can use and activate it.
		if ( !isset( $themes[$theme_dir] ) ) {
			$theme_exists = false;
			foreach ( $themes as $themedir ) {
				if ( file_exists( Utils::end_in_slash( $themedir ) . 'theme.xml' ) ) {
					$theme_dir = basename( $themedir );
					EventLog::log( _t( "Active theme directory no longer available.  Falling back to '{$theme_dir}'" ), 'err', 'theme', 'habari' );
					Options::set( 'theme_dir', basename( $themedir ) );
					$fallback_theme = Themes::create();
					Plugins::act_id( 'theme_activated', $fallback_theme->plugin_id(), $theme_dir, $fallback_theme );
					$theme_exists = true;
					// Refresh to the newly "activated" theme to ensure it takes the options that have just been set above and doesn't produce any errors.
					Utils::redirect();
					break;
				}
			}
			if ( !$theme_exists ) {
				die( _t( 'There is no valid theme currently installed.' ) );
			}
		}
		return $themes[$theme_dir];
	}

	/**
	 * Returns the active theme information from the database
	 * @params boolean $nopreview If true, return the real active theme, not the preview
	 * @return QueryRecord An array of Theme data
	 **/
	public static function get_active( $nopreview = false )
	{
		$theme = array();
		$theme['theme_dir'] = Themes::get_active_theme_dir( $nopreview );

		$data = simplexml_load_file( Utils::end_in_slash( $theme['theme_dir'] ) . 'theme.xml' );
		foreach ( $data as $name=>$value ) {
			$theme[$name] = (string) $value;
		}
		$theme['xml'] = $data;
		return $theme;
	}

	private static function get_by_name($name) {
		$themes = self::get_all_data();
		foreach($themes as $theme) {
			if($name == $theme['info']->name) {
				return $theme;
			}
		}
		return false;
	}


	/**
	 * Returns theme information for the active theme -- dir, path, theme.xml, screenshot url
	 * @params boolean $nopreview If true, return the real active theme, not the preview
	 * @return array An array of Theme data
	 */
	public static function get_active_data( $nopreview = false )
	{
		$all_data = Themes::get_all_data();
		$active_theme_dir = basename( Themes::get_active_theme_dir( $nopreview ) );
		$active_data = $all_data[$active_theme_dir];
		return $active_data;
	}

	/**
	 * function activate_theme
	 * Updates the database with the name of the new theme to use
	 * @param string the name of the theme
	**/
	public static function activate_theme( $theme_name, $theme_dir )
	{
		$ok = true;
		$ok = Plugins::filter( 'activate_theme', $ok, $theme_name ); // Allow plugins to reject activation
		if($ok) {
			$old_active_theme = Themes::create();
			Plugins::act_id( 'theme_deactivated', $old_active_theme->plugin_id(), $old_active_theme->name, $old_active_theme ); // For the theme itself to react to its deactivation
			Plugins::act( 'theme_deactivated_any', $old_active_theme->name, $old_active_theme ); // For any plugin to react to its deactivation
			Options::set( 'theme_name', $theme_name );
			Options::set( 'theme_dir', $theme_dir );
			$new_active_theme = Themes::create();
			
			// Set version of theme if it wasn't installed before
			$versions = Options::get( 'pluggable_versions' );
			if(!isset($versions[get_class($new_active_theme)])) {
				$versions[get_class($new_active_theme)] = $new_active_theme->get_version();
				Options::set( 'pluggable_versions', $versions );
			}
			
			// Run activation hooks for theme activation
			Plugins::act_id( 'theme_activated', $new_active_theme->plugin_id(), $theme_name, $new_active_theme ); // For the theme itself to react to its activation
			Plugins::act( 'theme_activated_any', $theme_name, $new_active_theme ); // For any plugin to react to its activation
			EventLog::log( _t( 'Activated Theme: %s', array( $theme_name ) ), 'notice', 'theme', 'habari' );
		}
		return $ok;
	}
	
	/**
	 * Sets a theme to be the current user's preview theme
	 * 
	 * @param string $theme_name The name of the theme to preview
	 * @param string $theme_dir The directory of the theme to preview
	 */
	public static function preview_theme( $theme_name, $theme_dir )
	{
		$_SESSION['user_theme_name'] = $theme_name;
		$_SESSION['user_theme_dir'] = $theme_dir;
		// Execute the theme's activated action
		$preview_theme = Themes::create();
		Plugins::act_id( 'theme_activated', $preview_theme->plugin_id(), $theme_name, $preview_theme );
		EventLog::log( _t( 'Previewed Theme: %s', array( $theme_name ) ), 'notice', 'theme', 'habari' );
	}
	
	/**
	 * Cancel the viewing of any preview theme
	 */
	public static function cancel_preview()
	{
		if ( isset( $_SESSION['user_theme_name'] ) ) {
			// Execute the theme's deactivated action
			$preview_theme = Themes::create();
			Plugins::act_id( 'theme_deactivated', $preview_theme->plugin_id() );
			EventLog::log( _t( 'Canceled Theme Preview: %s', array( $_SESSION['user_theme_name'] ) ), 'notice', 'theme', 'habari' );
			unset( $_SESSION['user_theme_name'] );
			unset( $_SESSION['user_theme_dir'] );
		}
	}

	/**
	 * Returns a named Theme descendant.
	 * If no parameter is supplied, then
	 * load the active theme from the database.
	 *
	 * If no theme option is set, a fatal error is thrown
	 *
	 * @param name            ( optional ) override the default theme lookup
	 * @param template_engine ( optional ) specify a template engine
	 * @param theme_dir       ( optional ) specify a theme directory
	 **/
	public static function create( $name = null, $template_engine = null, $theme_dir = null )
	{
		// If the name is not supplied, load the active theme
		if(empty($name)) {
			$themedata = self::get_active();
			if ( empty( $themedata ) ) {
				die( _t( 'Theme not installed.' ) );
			}
		}
		// Otherwise, try to load the named theme from user themes that are present
		else {
			$themedata = self::get_by_name( $name );
		}
		// If a theme wasn't found by name, create a blank object
		if(!$themedata) {
			$themedata = array();
			$themedata['name'] = $name;
			$themedata['version'] = 0;
		}
		// If a specific template engine was supplied, use it
		if(!empty($template_engine)) {
			$themedata['template_engine'] = $template_engine;
		}
		// If a theme directory was supplied, use the directory that was supplied
		if(!empty($theme_dir)) {
			$themedata['theme_dir'] = $theme_dir;
		}

		// Set the default theme file
		$themefile = 'theme.php';
		if(isset($themedata['info']->class['file']) && (string)$themedata['info']->class['file'] != '') {
			$themefile = (string)$themedata->xml->class['file'];
		}

		// Convert themedata to QueryRecord for legacy purposes
		// @todo: Potentially break themes by sending an array to the constructor instead of this QueryRecord
		$themedata = new QueryRecord($themedata);


		// Deal with parent themes
		if(isset($themedata->parent)) {
			// @todo If the parent theme doesn't exist, provide a useful error
			$parent_data = self::get_by_name( $themedata->parent );
			$parent_themefile = 'theme.php';
			if(isset($parent_data['info']->class['file']) && (string)$parent_data['info']->class['file'] != '') {
				$parent_themefile = (string)$parent_data['info']->class['file'];
			}
			include_once($parent_data['theme_dir'] . $parent_themefile);

			$themedata->parent_theme_dir = Utils::single_array($parent_data['theme_dir']);
			$themedata->theme_dir = array_merge(Utils::single_array($themedata->theme_dir), $themedata->parent_theme_dir);
		}
		$primary_theme_dir = $themedata->theme_dir;
		$primary_theme_dir = is_array($primary_theme_dir) ? reset($primary_theme_dir) : $primary_theme_dir;

		// Include the theme class file
		if ( file_exists( $primary_theme_dir . $themefile ) ) {
			include_once( $primary_theme_dir . $themefile );
		}

		if ( isset( $themedata->class ) ) {
			$classname = $themedata->class;
		}
		else {
			$classname = self::class_from_filename( $primary_theme_dir . $themefile );
		}

		// the final fallback, for the admin "theme"
		if ( $classname == '' ) {
			$classname = 'Theme';
		}

		$created_theme = new $classname( $themedata );
		$created_theme->upgrade();
		Plugins::act_id( 'init_theme', $created_theme->plugin_id(), $created_theme );
		Plugins::act( 'init_theme_any', $created_theme );
		return $created_theme;

	}

	public static function class_from_filename( $file, $check_realpath = false )
	{
		if ( $check_realpath ) {
			$file = realpath( $file );
		}

		$theme_classes = self::get_theme_classes();
		foreach ( $theme_classes as $theme ) {
			$class = new ReflectionClass( $theme );
			$classfile = str_replace( '\\', '/', $class->getFileName() );
			if ( $classfile == $file ) {
				return $theme;
			}
		}
		// if we haven't found the plugin class, try again with realpath resolution:
		if ( $check_realpath ) {
			// really can't find it
			return false;
		}
		else {
			return self::class_from_filename( $file, true );
		}
	}

	/**
	 * Get a list of classes that extend Theme
	 * @static
	 * @return array List of string names of classes that extend Theme
	 */
	public static function get_theme_classes()
	{
		$classes = get_declared_classes();
		foreach($classes as $class) {
			$parents = class_parents( $class, false );
			if(count($parents) > 0) {
				$class_parents[$class] = $parents;
			}
		}

		$theme_classes = array();
		do {
			$delta = count($theme_classes);
			foreach($class_parents as $class => $parents) {
				if(count(array_intersect($theme_classes + array('Theme'), $parents))>0) {
					$theme_classes[$class] = $class;
				}
			}
		} while($delta != count($theme_classes));
		return $theme_classes;
	}
}

?>
