<?php
/*
Plugin Name: WP_Meta_Box
Plugin URI: http://wordpress.org/
Description: The WP_Meta_Box class in development for 3.3.
Version: 0.1
Author: The WordPress Team
*/

class WP_Meta_Box {
	/**
	 * The screen where the meta box will be shown.
	 */
	public $screen;

	/**
	 * A unique slug to allow multiple meta boxes of the same class
	 * to exist on the same screen.
	 */
	public $slug;

	/**
	 * A unique identifier for the meta box.
	 *
	 * A string composed from the meta box class, screen, and slug.
	 */
	public $unique_id;

	/**
	 * Arguments passed to the meta box constructor.
	 */
	public $args;

	/**
	 * A registry that tracks each meta box instance.
	 */
	public static $registry;


	/**
	 * Constructor.
	 *
	 * Any subclasses MUST call parent::_construct( $screen, $args ).
	 *
	 * @param string $screen - The ID (string) for the screen where the meta box will be shown.
	 * @param array  $args - An array of supplied arguments. Optional.
	 *
	 * $args recognized by default:
	 *    title    - The title of the meta box.
	 *    context  - The context within the page where the box should show ('normal', 'advanced').
	 *    priority - The priority within the context where the boxes should show ('high', 'low').
	 *    slug     - A unique string to identify the meta box.
	 */
	public function __construct( $screen, $args = array() ) {
		// Set variables
		$this->screen = $screen;
		$this->slug   = empty( $args['slug'] ) ? '' : $args['slug'];

		// Remove slug from args
		unset( $args['slug'] );

		// Set default args
		$defaults = array(
			'title'         => __( 'Untitled' , 'wordcampbase'),
			'context'       => 'side',
			'priority'      => 'default',
		);
		$this->args = wp_parse_args( $args, $defaults );

		// Add the meta box to the registry.
		// Generates a slug if one doesn't already exist.
		WP_Meta_Box::registry()->add( $this );

		// Construct the meta box's unique ID.
		$this->unique_id = get_class( $this ) . "_{$this->screen}_{$this->slug}";

		// Bind hooks
		add_action( 'admin_init',            array( $this, '_register' ) );
		add_action( 'admin_enqueue_scripts', array( $this, '_enqueue_scripts' ) );
	}

	/**
	 * Fetches the meta box registry.
	 */
	public static function registry() {
		if ( ! isset( WP_Meta_Box::$registry ) )
			WP_Meta_Box::$registry = new WP_Meta_Box_Registry();
		return WP_Meta_Box::$registry;
	}

	/**
	 * Remove the meta box.
	 */
	public function remove() {
		// Make sure we've removed any data from the registry.
		WP_Meta_Box::registry()->remove( get_class( $this ), $this->screen, $this->slug );

		// Unbind actions.
		remove_action( 'admin_init',            array( $this, '_register' ) );
		remove_action( 'admin_enqueue_scripts', array( $this, '_enqueue_scripts' ) );
	}

	/**
	 * Render the meta box. Implement in a subclass.
	 */
	protected function render() {}

	/**
	 * Save the meta box. Implement in a subclass.
	 */
	protected function save() {}

	/**
	 * Fires when the meta box is registered. Override in a subclass.
	 */
	protected function register() {}

	/**
	 * Enqueue meta box scripts. Override in a subclass.
	 */
	protected function enqueue_scripts() {}

	/**
	 * Determine whether the meta box will be saved. Override in a subclass.
	 *
	 * Return true to save, false to cancel.
	 */
	protected function maybe_save() {
		return true;
	}

	/**
	 * Add an action to trigger save.
	 *
	 * Due to the different types of meta boxes, save will not be triggered by default.
	 */
	public function add_save_action( $name, $priority=10 ) {
		add_action( $name, array( $this, '_save' ), $priority, 99 ); // Effectively infinite args.
	}

	/**
	 * Remove a save action.
	 */
	public function remove_save_action( $name, $priority=10 ) {
		remove_action( $name, array( $this, '_save' ), $priority, 99 ); // Effectively infinite args.
	}


	/* =====================================================================
	 * INTERNAL FUNCTIONS
	 * ===================================================================== */

	/**
	 * Internal function. Registers the meta box.
	 */
	public final function _register() {
		$id = "{$this->unique_id}-meta-box";

		add_meta_box( $id, $this->args['title'], array( $this, '_render' ),
			$this->screen, $this->args['context'], $this->args['priority'],
			$this->args );

		$this->register();
	}

	/**
	 * Internal function. Ensures scripts are only loaded when necessary.
	 */
	public final function _enqueue_scripts() {
		$current_screen = get_current_screen();

		if ( isset( $current_screen ) && $this->screen == $current_screen->id )
			$this->enqueue_scripts();
	}

	/**
	 * Internal function, initiates the rendering process.
	 */
	public final function _render( $object, $box ) {
		wp_nonce_field( "{$this->unique_id}_nonce", "_wpnonce_{$this->unique_id}", false );

		$this->render();
	}

	/**
	 * Internal function, initiates the saving process.
	 */
	public final function _save() {
		// Nonce check (sorry, you don't have a choice about this one).
		check_admin_referer( "{$this->unique_id}_nonce", "_wpnonce_{$this->unique_id}" );

		$args = func_get_args();

		// Only save if maybe_save returns true.
		if ( call_user_func_array( array( $this, 'maybe_save' ), $args ) )
			call_user_func_array( array( $this, 'save' ), $args );
	}
}



class WP_Meta_Box_Registry {
	public $instances = array();

	/**
	 * Adds a meta box instance.
	 *
	 * If $instance->slug is defined, will use $slug.
	 * If a meta box with the same slug exists, it will be overwritten.
	 */
	public function add( $instance ) {
		$class = get_class( $instance );

		if ( ! isset( $this->instances[ $class ] ) )
			$this->instances[ $class ] = array();

		if ( ! isset( $this->instances[ $class ][ $instance->screen ] ) )
			$this->instances[ $class ][ $instance->screen ] = array();

		$screen_instances = $this->instances[ $class ][ $instance->screen ];

		// If no slug is specified, get the numerical index.
		if ( empty( $instance->slug ) ) {
			$this->instances[ $class ][ $instance->screen ][] = $instance;

			// Get the index of the instance. I feel dirty.
			$instance->slug = array_pop( array_keys( $this->instances[ $class ][ $instance->screen ] ) );

		} else {
			$this->instances[ $class ][ $instance->screen ][ $instance->slug ] = $instance;
		}
	}

	public function get( $class, $screen = false, $slug = false ) {
		if ( ! isset( $this->instances[ $class ] ) )
			return array();

		if ( empty( $screen ) )
			return $this->instances[ $class ];

		if ( ! isset( $this->instances[ $class ][ $screen ] ) )
			return array();

		$screen_instances = $this->instances[ $class ][ $screen ];

		if ( empty( $slug ) )
			return $screen_instances;

		if ( isset( $screen_instances[ $slug ] ) )
			return $screen_instances[ $slug ];
		else
			return false;
	}

	public function remove( $class, $screen = false, $slug = false ) {
		// Ensure that the instance(s) we want to remove exist.
		$instances = $this->get( $class, $screen, $slug );
		if ( empty( $instances ) )
			return;

		// Remove the appropriate instance(s) from the registry.
		if ( empty( $screen ) )
			unset( $this->instances[ $class ] );
		elseif ( empty( $slug ) )
			unset( $this->instances[ $class ][ $screen ] );
		else
			unset( $this->instances[ $class ][ $screen ][ $slug ] );

		// Call remove on each instance.
		if ( ! empty( $slug ) )
			$instances = array( $instances );

		foreach ( $instances as $instance ) {
			$instance->remove();
		}
	}
}



class WP_CPT_Meta_Box extends WP_Meta_Box {
	function __construct( $screen, $args = array() ) {
		// Call the WP_Meta_Box constructor.
		parent::__construct( $screen, $args );

		// Save the meta box contents when 'save_post' is triggered.
		$this->add_save_action( 'save_post' );
	}

	function maybe_save( $post_id, $post ) {
		// Bail if we're autosaving
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
			return;

		// @TODO Add revision check

		// Cap check
		if ( ! current_user_can( 'edit_post', $post_id ) )
			return;

		return true;
	}
}
