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

/**
 * Pluggable class
 * Implements methods that allow descendant classes to register functions to plugin hooks
 *
 * @version $Id$
 * @copyright 2008
 */
abstract class Pluggable
{
	private $_class_name = null;
	public $info;
	public $plugin_id;
	private $_new_rules = array();
	protected $_added_templates = array();

	/**
	 * Pluggable constructor.
	 * This function creates some internal structures that are required for plugin processing
	 * Plugins should not define their own constructors, because they are instantiated
	 * to extract plugin info.  Instead, include a sink for a "init" hook
	 * which is executed immediately after the plugin is loaded during normal execution.
	 */
	public function __construct()
	{
		$this->info = $this->info();
		$this->plugin_id = $this->plugin_id();
	}

	/**
	 * Gets the filename that contains this pluggable class
	 * @return string The filename of the file that contains the pluggable class.
	 */
	final public function get_file()
	{
		if ( empty( $this->_class_name ) ) {
			$class = new ReflectionClass( get_class( $this ) );
			$this->_class_name = $class->getFileName();
		}
		return $this->_class_name;
	}

	/**
	 * Gets a database schema associated with this pluggable
	 * @return string The database schema
	 */
	final public function get_db_schema()
	{
		$db = DB::get_driver_name();
		$schema = dirname( $this->get_file() ) . '/schema/' . $db . '.sql';
		return file_get_contents( $schema );
	}

	/**
	 * Get a fully-qualified URL directory that contains this pluggable class
	 *
	 * @param bool whether to include a trailing slash.  Default: No
	 * @return string URL
	 */
	public function get_url( $trail = false )
	{
		return URL::get_from_filesystem( $this->get_file(), $trail );
	}

	/**
	 * Returns a unique id for this pluggable
	 * @return string A plugin id
	 */
	final public function plugin_id()
	{
		static $id;
		if ( !isset( $id ) ) {
			$id = Plugins::id_from_file( str_replace( '\\', '/', $this->get_file() ) );
		}
		return $id;
	}

	/**
	 * Load a translation domain/file for this pluggable
	 * @return boolean true if data was successfully loaded, false otherwise
	 */
	public function load_text_domain( $domain )
	{
		$base_dir = realpath( dirname( $this->get_file() ) );

		return HabariLocale::load_pluggable_domain( $domain, $base_dir );
	}
	
	/**
	 * Registers all of this class' action_ and filter_ functions with the Plugins dispatcher
	 * Registers xmlrpc_ functions with the Plugins dispatcher, and turns '__' into '.'
	 *   for the purposes of matching dotted XMLRPC requests.
	 * If the class is an instance of Pluggable, registers the hooks with a plugin id also.
	 * @param mixed $object The object or class name to register the hooks of
	 **/
	public static function load_hooks($object)
	{
		static $registered = array();
		if(is_object($object)) {
			$hash = spl_object_hash($object);
			if(isset($registered[$hash])) {
				return;
			}
			else {
				$registered[$hash] = true;
			}
		}
		else {
			$registered[$object] = true;
		}
		
		// combine the array so we can have hooks => function
		$methods = get_class_methods( $object );
		$methods = array_combine( $methods, $methods );
		// get the specific priority values for functions, as needed
		if ( method_exists( $object, 'set_priorities' ) ) {
			$priorities = $object->set_priorities();
		}
		// get the aliases.
		if ( method_exists( $object, 'alias' ) ) {
			$methods = array_merge_recursive( $methods, $object->alias() );
		}
		// loop over all the methods in this class
		foreach ( $methods as $fn => $hooks ) {
			// loop hooks and register callback for each
			foreach ( (array) $hooks as $hook ) {
				// make sure the method name is of the form
				// action_foo or filter_foo of xmlrpc_foo or theme_foo
				if ( preg_match( '#^(action|filter|xmlrpc|theme)_#i', $hook ) ) {
					$priority = 8;
					if(isset($priorities[$hook])) {
						$priority = $priorities[$hook];
					}
					elseif(preg_match('#^(.+)_(\d+)$#', $hook, $priority_match)) {
						$hook = $priority_match[1];
						$priority = intval($priority_match[2]);
					}
					elseif(isset( $priorities[$fn])) {
						$priority = $priorities[$fn];
					}
					list( $type, $hook ) = explode( '_', $hook, 2 );
					if ( $type === 'xmlrpc' ) {
						$hook = str_replace( '__', '.', $hook );
					}
					Plugins::register( array( $object, $fn ), $type, $hook, $priority );
					if($object instanceof Pluggable) {
						Plugins::register( array( $object, $fn ), $type, $hook . ':' . $object->plugin_id(), $priority );
					}
				}
			}
		}
	}

	/**
	 * Called when a pluggable is loaded to register its actions and filters.
	 */
	public function load()
	{
		self::load_hooks($this);
		// look for help with this
		if ( method_exists( $this, 'help' ) ) {
			Plugins::register( array( $this, '_help_plugin_config' ), 'filter', 'plugin_config:' . $this->plugin_id(), 8 );
			Plugins::register( array( $this, '_help_plugin_ui' ), 'action', 'plugin_ui:' . $this->plugin_id(), 8 );
		}
		// look for a basic configure method
		if ( method_exists( $this, 'configure' ) ) {
			Plugins::register( array( $this, '_configure_plugin_config' ), 'filter', 'plugin_config:' . $this->plugin_id(), 8 );
			Plugins::register( array( $this, '_configure_plugin_ui' ), 'action', 'plugin_ui:' . $this->plugin_id(), 8 );
		}
	}

	/**
	 * Registered to the plugin_config hook to supply help via a plugin's help() method
	 *
	 * @param array $actions An array of actions applicable to this plugin
	 * @param string $plugin_id The plugin id to which the actions belong
	 * @return array The modified array of actions
	 */
	public function _help_plugin_config( $actions, $plugin_id )
	{
		if ( $plugin_id == $this->plugin_id() ) {
			$actions['_help'] = _t( '?' );
		}
		return $actions;
	}

	/**
	 * Registered to the plugin_ui hook to supply help via a plugin's help() method
	 *
	 * @param string $plugin_id The id of the plugin whose action was triggered
	 * @param string $action The action triggered
	 */
	public function _help_plugin_ui( $plugin_id, $action )
	{
		if ( $plugin_id == $this->plugin_id() && $action == '_help' ) {
			$output = $this->help();
			if ( $output instanceof FormUI ) {
				$output->out();
			}
			else {
				echo "<div class=\"help\">{$output}</div>";
			}
		}
	}

	/**
	 * Registered to the plugin_config hook to supply a config via a plugin's configure() method
	 *
	 * @param array $actions An array of actions applicable to this plugin
	 * @param string $plugin_id The plugin id to which the actions belong
	 * @return array The modified array of actions
	 */
	public function _configure_plugin_config( $actions, $plugin_id )
	{
		if ( $plugin_id == $this->plugin_id() ) {
			$actions['_configure'] = _t( 'Configure' );
		}
		return $actions;
	}

	/**
	 * Registered to the plugin_ui hook to supply a config via a plugin's configure() method
	 *
	 * @param string $plugin_id The id of the plugin whose action was triggered
	 * @param string $action The action triggered
	 */
	public function _configure_plugin_ui( $plugin_id, $action )
	{
		if ( $plugin_id == $this->plugin_id() && $action == '_configure' ) {
			$output = $this->configure();
			if ( $output instanceof FormUI ) {
				$output->out();
			}
			else {
				echo $output;
			}
		}
	}


	/**
	 * Add a rewrite rule that dispatches entirely to a plugin hook
	 *
	 * @param mixed $rule An old-style rewrite rule string, where quoted segments are literals and unquoted segments are variable names, OR a RewriteRule object
	 * @param string $hook The suffix of the hook function: action_plugin_act_{$suffix}
	 */
	public function add_rule( $rule, $hook )
	{
		if ( count( $this->_new_rules ) == 0 ) {
			Plugins::register( array( $this, '_filter_rewrite_rules' ), 'filter', 'rewrite_rules', 7 );
		}
		if ( $rule instanceof RewriteRule ) {
			$this->_new_rules[] = $rule;
		}
		else {
			$this->_new_rules[] = RewriteRule::create_url_rule( $rule, 'PluginHandler', $hook );
		}
	}

	/**
	 * Add the rewrite rules queued by add_rule() to the full rule set
	 *
	 * @param array $rules The array of current RewriteRules
	 * @return array The appended array of RewriteRules
	 */
	public function _filter_rewrite_rules( $rules )
	{
		$rules = array_merge( $rules, $this->_new_rules );
		return $rules;
	}

	/**
	 * Adds a template to the default theme that is stored in a specified path.
	 * Use this function as a shortcut to make available additional templates to a theme
	 * from within the plugin directory.
	 *
	 * @param string $name The name of the template that will be displayed, sans extension
	 * @param string $filename The full path of the template file used for the specified name
	 * @param boolean $override If false, allow a template with the same name in the active theme directory to override this one.
	 * If true, always override the active theme's template with this one.
	 */
	protected function add_template( $name, $filename, $override = false )
	{
		if ( count( $this->_added_templates ) == 0 ) {
			Plugins::register( array( &$this, '_plugin_available_templates' ), 'filter', 'available_templates' );
			Plugins::register( array( &$this, '_plugin_include_template_file' ), 'filter', 'include_template_file' );
		}

		$this->_added_templates[$name] = array( $filename, $override );
	}

	/**
	 * Add plugin templates to the list of templates that are present in the current theme
	 *
	 * @param array $list List of template names in the current theme
	 * @return array The modified list of template names
	 */
	public function _plugin_available_templates( $list )
	{
		$list = array_merge( $list, array_keys( $this->_added_templates ) );
		return $list;
	}

	/**
	 * Potentially serve a different file for the requested template name
	 *
	 * @param string $file The filename of the template the theme will display
	 * @param string $name The name of the template requested
	 * @return string The potentially modified filename to use for the requested template.
	 */
	public function _plugin_include_template_file( $file, $name )
	{
		if ( isset( $this->_added_templates[$name] ) ) {
			if ( $this->_added_templates[$name][1] || !file_exists( $file ) ) {
				$file = $this->_added_templates[$name][0];
			}
		}
		return $file;
	}

	/** 
	 * Provide a method to return the version number from a pluggable's info
	 * @return string The version of the pluggable
	 **/
	public abstract function get_version();
	
	/**
	 * Execute the upgrade action on any pluggable that has a version number change
	 * Update the version number of the pluggable in the database to what is installed
	 */
	public function upgrade()
	{
		if(DB::is_connected() && @ Options::get( 'installed' )) {
			$pluggable_class = get_class($this);
			$versions = Options::get( 'pluggable_versions' );
			if(isset($versions[$pluggable_class])) {
				$old_version = $versions[$pluggable_class];
				if($old_version != $this->get_version()) {
					Plugins::act_id('upgrade', $this->plugin_id(), $old_version);
					$versions[$pluggable_class] = $this->get_version();
					Options::set( 'pluggable_versions', $versions );
				}
			}
			else {
				$versions[$pluggable_class] = $this->get_version();
				Options::set( 'pluggable_versions', $versions );
			}
		}
	}
}

?>
