<?php
/**
 * Primary plugin class.  This sets up and runs the show.
 *
 * @package    ButterBean
 * @author     Justin Tadlock <justin@justintadlock.com>
 * @copyright  Copyright (c) 2015-2016, Justin Tadlock
 * @link       https://github.com/justintadlock/butterbean
 * @license    http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 */

if ( ! class_exists( 'ButterBean' ) ) {

	/**
	 * Main ButterBean class.  Runs the show.
	 *
	 * @since  1.0.0
	 * @access public
	 */
	final class ButterBean {

		/**
		 * Directory path to the plugin folder.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    string
		 */
		public $dir_path = '';

		/**
		 * Directory URI to the plugin folder.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    string
		 */
		public $dir_uri = '';

		/**
		 * Directory path to the template folder.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    string
		 */
		public $tmpl_path = '';

		/**
		 * Post id.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    int
		 */
		public $post_id = '';

		/**
		 * Array of managers.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    array
		 */
		public $managers = array();

		/**
		 * Array of manager types.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    array
		 */
		public $manager_types = array();

		/**
		 * Array of section types.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    array
		 */
		public $section_types = array();

		/**
		 * Array of control types.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    array
		 */
		public $control_types = array();

		/**
		 * Array of setting types.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    array
		 */
		public $setting_types = array();

		/**
		 * Whether this is a new post.  Once the post is saved and we're
		 * no longer on the `post-new.php` screen, this is going to be
		 * `false`.
		 *
		 * @since  1.0.0
		 * @access public
		 * @var    bool
		 */
		public $is_new_post = false;

		/**
		 * Returns the instance.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return object
		 */
		public static function get_instance() {

			static $instance = null;

			if ( is_null( $instance ) ) {
				$instance = new self;
				$instance->setup();
				$instance->includes();
				$instance->setup_actions();
			}

			return $instance;
		}

		/**
		 * Constructor method.
		 *
		 * @since  1.0.0
		 * @access private
		 * @return void
		 */
		private function __construct() {}

		/**
		 * Initial plugin setup.
		 *
		 * @since  1.0.0
		 * @access private
		 * @return void
		 */
		private function setup() {

			$this->dir_path  = apply_filters( 'butterbean_dir_path', trailingslashit( plugin_dir_path( __FILE__ ) ) );
			$this->dir_uri   = apply_filters( 'butterbean_dir_uri',  trailingslashit( plugin_dir_url(  __FILE__ ) ) );

			$this->tmpl_path = trailingslashit( $this->dir_path . 'tmpl' );
		}

		/**
		 * Loads include and admin files for the plugin.
		 *
		 * @since  1.0.0
		 * @access private
		 * @return void
		 */
		private function includes() {

			// If not in the admin, bail.
			if ( ! is_admin() )
				return;

			// Load base classes.
			require_once( $this->dir_path . 'inc/class-manager.php' );
			require_once( $this->dir_path . 'inc/class-section.php' );
			require_once( $this->dir_path . 'inc/class-control.php' );
			require_once( $this->dir_path . 'inc/class-setting.php' );

			// Load control sub-classes.
			require_once( $this->dir_path . 'inc/controls/class-control-checkboxes.php'    );
			require_once( $this->dir_path . 'inc/controls/class-control-color.php'         );
			require_once( $this->dir_path . 'inc/controls/class-control-datetime.php'      );
			require_once( $this->dir_path . 'inc/controls/class-control-image.php'         );
			require_once( $this->dir_path . 'inc/controls/class-control-palette.php'       );
			require_once( $this->dir_path . 'inc/controls/class-control-radio.php'         );
			require_once( $this->dir_path . 'inc/controls/class-control-radio-image.php'   );
			require_once( $this->dir_path . 'inc/controls/class-control-select-group.php'  );
			require_once( $this->dir_path . 'inc/controls/class-control-textarea.php'      );

			require_once( $this->dir_path . 'inc/controls/class-control-excerpt.php'       );
			require_once( $this->dir_path . 'inc/controls/class-control-multi-avatars.php' );
			require_once( $this->dir_path . 'inc/controls/class-control-parent.php'        );

			// Load setting sub-classes.
			require_once( $this->dir_path . 'inc/settings/class-setting-multiple.php' );
			require_once( $this->dir_path . 'inc/settings/class-setting-datetime.php' );
			require_once( $this->dir_path . 'inc/settings/class-setting-array.php'    );

			// Load functions.
			require_once( $this->dir_path . 'inc/functions-core.php' );
		}

		/**
		 * Sets up initial actions.
		 *
		 * @since  1.0.0
		 * @access private
		 * @return void
		 */
		private function setup_actions() {

			// Call the register function.
			add_action( 'load-post.php',     array( $this, 'register' ), 95 );
			add_action( 'load-post-new.php', array( $this, 'register' ), 95 );

			// Register default types.
			add_action( 'butterbean_register', array( $this, 'register_manager_types' ), -95 );
			add_action( 'butterbean_register', array( $this, 'register_section_types' ), -95 );
			add_action( 'butterbean_register', array( $this, 'register_control_types' ), -95 );
			add_action( 'butterbean_register', array( $this, 'register_setting_types' ), -95 );
		}

		/**
		 * Registration callback. Fires the `butterbean_register` action hook to
		 * allow plugins to register their managers.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function register() {

			// If this is a new post, set the new post boolean.
			if ( 'load-post-new.php' === current_action() )
				$this->is_new_post = true;

			// Get the current post type.
			$post_type = get_current_screen()->post_type;

			// Action hook for registering managers.
			do_action( 'butterbean_register', $this, $post_type );

			// Loop through the managers to see if we're using on on this screen.
			foreach ( $this->managers as $manager ) {

				// If we found a matching post type, add our actions/filters.
				if ( ! in_array( $post_type, (array) $manager->post_type ) ) {
					$this->unregister_manager( $manager->name );
					continue;
				}

				// Sort controls and sections by priority.
				uasort( $manager->controls, array( $this, 'priority_sort' ) );
				uasort( $manager->sections, array( $this, 'priority_sort' ) );
			}

			// If no managers registered, bail.
			if ( ! $this->managers )
				return;

			// Add meta boxes.
			add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 5 );

			// Save settings.
			add_action( 'save_post', array( $this, 'update' ) );

			// Load scripts and styles.
			add_action( 'admin_enqueue_scripts',      array( $this, 'enqueue_scripts' ) );
			add_action( 'butterbean_enqueue_scripts', array( $this, 'enqueue'         ) );

			// Localize scripts and Undescore templates.
			add_action( 'admin_footer', array( $this, 'localize_scripts' ) );
			add_action( 'admin_footer', array( $this, 'print_templates'  ) );

			// Renders our Backbone views.
			add_action( 'admin_print_footer_scripts', array( $this, 'render_views' ), 95 );
		}

		/**
		 * Register a manager.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  object|string  $manager
		 * @param  array          $args
		 * @return void
		 */
		public function register_manager( $manager, $args = array() ) {

			if ( ! is_object( $manager ) ) {

				$type = isset( $args['type'] ) ? $this->get_manager_type( $args['type'] ) : $this->get_manager_type( 'default' );

				$manager = new $type( $manager, $args );
			}

			if ( ! $this->manager_exists( $manager->name ) )
				$this->managers[ $manager->name ] = $manager;

			return $manager;
		}

		/**
		 * Unregisters a manager object.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $name
		 * @return void
		 */
		public function unregister_manager( $name ) {

			if ( $this->manager_exists( $name ) )
				unset( $this->managers[ $name ] );
		}

		/**
		 * Returns a manager object.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $name
		 * @return object|bool
		 */
		public function get_manager( $name ) {

			return $this->manager_exists( $name ) ? $this->managers[ $name ] : false;
		}

		/**
		 * Checks if a manager exists.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $name
		 * @return bool
		 */
		public function manager_exists( $name ) {

			return isset( $this->managers[ $name ] );
		}

		/**
		 * Registers a manager type.  This is just a method of telling ButterBean
		 * the class of your custom manager type.  It allows the manager to be
		 * called without having to pass an object to `register_manager()`.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @param  string  $class
		 * @return void
		 */
		public function register_manager_type( $type, $class ) {

			if ( ! $this->manager_type_exists( $type ) )
				$this->manager_types[ $type ] = $class;
		}

		/**
		 * Unregisters a manager type.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return void
		 */
		public function unregister_manager_type( $type ) {

			if ( $this->manager_type_exists( $type ) )
				unset( $this->manager_types[ $type ] );
		}

		/**
		 * Returns the class name for the manager type.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return string
		 */
		public function get_manager_type( $type ) {

			return $this->manager_type_exists( $type ) ? $this->manager_types[ $type ] : $this->manager_types[ 'default' ];
		}

		/**
		 * Checks if a manager type exists.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return bool
		 */
		public function manager_type_exists( $type ) {

			return isset( $this->manager_types[ $type ] );
		}

		/**
		 * Registers a section type.  This is just a method of telling ButterBean
		 * the class of your custom section type.  It allows the section to be
		 * called without having to pass an object to `register_section()`.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @param  string  $class
		 * @return void
		 */
		public function register_section_type( $type, $class ) {

			if ( ! $this->section_type_exists( $type ) )
				$this->section_types[ $type ] = $class;
		}

		/**
		 * Unregisters a section type.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return void
		 */
		public function unregister_section_type( $type ) {

			if ( $this->section_type_exists( $type ) )
				unset( $this->section_types[ $type ] );
		}

		/**
		 * Returns the class name for the section type.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return string
		 */
		public function get_section_type( $type ) {

			return $this->section_type_exists( $type ) ? $this->section_types[ $type ] : $this->section_types[ 'default' ];
		}

		/**
		 * Checks if a section type exists.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return bool
		 */
		public function section_type_exists( $type ) {

			return isset( $this->section_types[ $type ] );
		}

		/**
		 * Registers a control type.  This is just a method of telling ButterBean
		 * the class of your custom control type.  It allows the control to be
		 * called without having to pass an object to `register_control()`.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @param  string  $class
		 * @return void
		 */
		public function register_control_type( $type, $class ) {

			if ( ! $this->control_type_exists( $type ) )
				$this->control_types[ $type ] = $class;
		}

		/**
		 * Unregisters a control type.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return void
		 */
		public function unregister_control_type( $type ) {

			if ( $this->control_type_exists( $type ) )
				unset( $this->control_types[ $type ] );
		}

		/**
		 * Returns the class name for the control type.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return string
		 */
		public function get_control_type( $type ) {

			return $this->control_type_exists( $type ) ? $this->control_types[ $type ] : $this->control_types[ 'default' ];
		}

		/**
		 * Checks if a control type exists.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return bool
		 */
		public function control_type_exists( $type ) {

			return isset( $this->control_types[ $type ] );
		}

		/**
		 * Registers a setting type.  This is just a method of telling ButterBean
		 * the class of your custom setting type.  It allows the setting to be
		 * called without having to pass an object to `register_setting()`.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @param  string  $class
		 * @return void
		 */
		public function register_setting_type( $type, $class ) {

			if ( ! $this->setting_type_exists( $type ) )
				$this->setting_types[ $type ] = $class;
		}

		/**
		 * Unregisters a setting type.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return void
		 */
		public function unregister_setting_type( $type ) {

			if ( $this->setting_type_exists( $type ) )
				unset( $this->setting_types[ $type ] );
		}

		/**
		 * Returns the class name for the setting type.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return string
		 */
		public function get_setting_type( $type ) {

			return $this->setting_type_exists( $type ) ? $this->setting_types[ $type ] : $this->setting_types[ 'default' ];
		}

		/**
		 * Checks if a setting type exists.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $type
		 * @return bool
		 */
		public function setting_type_exists( $type ) {

			return isset( $this->setting_types[ $type ] );
		}

		/**
		 * Registers our manager types so that devs don't have to directly instantiate
		 * the class each time they register a manager.  Instead, they can use the
		 * `type` argument.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function register_manager_types() {

			$this->register_manager_type( 'default', 'ButterBean_Manager' );
		}

		/**
		 * Registers our section types so that devs don't have to directly instantiate
		 * the class each time they register a section.  Instead, they can use the
		 * `type` argument.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function register_section_types() {

			$this->register_section_type( 'default', 'ButterBean_Section' );
		}

		/**
		 * Registers our control types so that devs don't have to directly instantiate
		 * the class each time they register a control.  Instead, they can use the
		 * `type` argument.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function register_control_types() {

			$this->register_control_type( 'default',       'ButterBean_Control'               );
			$this->register_control_type( 'checkboxes',    'ButterBean_Control_Checkboxes'    );
			$this->register_control_type( 'color',         'ButterBean_Control_Color'         );
			$this->register_control_type( 'datetime',      'ButterBean_Control_Datetime'      );
			$this->register_control_type( 'excerpt',       'ButterBean_Control_Excerpt'       );
			$this->register_control_type( 'image',         'ButterBean_Control_Image'         );
			$this->register_control_type( 'palette',       'ButterBean_Control_Palette'       );
			$this->register_control_type( 'radio',         'ButterBean_Control_Radio'         );
			$this->register_control_type( 'radio-image',   'ButterBean_Control_Radio_Image'   );
			$this->register_control_type( 'select-group',  'ButterBean_Control_Select_Group'  );
			$this->register_control_type( 'textarea',      'ButterBean_Control_Textarea'      );
			$this->register_control_type( 'multi-avatars', 'ButterBean_Control_Multi_Avatars' );
			$this->register_control_type( 'parent',        'ButterBean_Control_Parent'        );
		}

		/**
		 * Registers our setting types so that devs don't have to directly instantiate
		 * the class each time they register a setting.  Instead, they can use the
		 * `type` argument.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function register_setting_types() {

			$this->register_setting_type( 'default',  'ButterBean_Setting'          );
			$this->register_setting_type( 'single',   'ButterBean_Setting'          );
			$this->register_setting_type( 'multiple', 'ButterBean_Setting_Multiple' );
			$this->register_setting_type( 'array',    'ButterBean_Setting_Array'    );
			$this->register_setting_type( 'datetime', 'ButterBean_Setting_Datetime' );
		}

		/**
		 * Fires an action hook to register/enqueue scripts/styles.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function enqueue_scripts() {

			do_action( 'butterbean_enqueue_scripts' );
		}

		/**
		 * Loads scripts and styles.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function enqueue() {
			$min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';

			// Enqueue the main plugin script.
			wp_enqueue_script( 'butterbean', $this->dir_uri . "js/butterbean{$min}.js", array( 'backbone', 'wp-util' ), '', true );

			// Enqueue the main plugin style.
			wp_enqueue_style( 'butterbean', $this->dir_uri . "css/butterbean{$min}.css" );

			// Loop through the manager and its controls and call each control's `enqueue()` method.
			foreach ( $this->managers as $manager ) {

				$manager->enqueue();

				foreach ( $manager->sections as $section )
					$section->enqueue();

				foreach ( $manager->controls as $control )
					$control->enqueue();
			}
		}

		/**
		 * Callback function for adding meta boxes.  This function adds a meta box
		 * for each of the managers.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  string  $post_type
		 * @return void
		 */
		public function add_meta_boxes( $post_type ) {

			foreach ( $this->managers as $manager ) {

				// If the manager is registered for the current post type, add a meta box.
				if ( in_array( $post_type, (array) $manager->post_type ) && $manager->check_capabilities() ) {

					add_meta_box(
						"butterbean-ui-{$manager->name}",
						$manager->label,
						array( $this, 'meta_box' ),
						$post_type,
						$manager->context,
						$manager->priority,
						array( 'manager' => $manager )
					);
				}
			}
		}

		/**
		 * Displays the meta box.  Note that the actual content of the meta box is
		 * handled via Underscore.js templates.  The only thing we're outputting here
		 * is the nonce field.
		 *
		 * @since  1.0.0
		 * @access public
		 * @param  object  $post
		 * @param  array   $metabox
		 * @return void
		 */
		public function meta_box( $post, $metabox ) {

			$manager = $metabox['args']['manager'];

			$manager->post_id = $this->post_id = $post->ID;

			// Nonce field to validate on save.
			wp_nonce_field( "butterbean_{$manager->name}_nonce", "butterbean_{$manager->name}" );
		}

		/**
		 * Passes the appropriate section and control json data to the JS file.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function localize_scripts() {

			$json = array( 'managers' => array() );

			foreach ( $this->managers as $manager ) {

				if ( $manager->check_capabilities() )
					$json['managers'][] = $manager->get_json();
			}

			wp_localize_script( 'butterbean', 'butterbean_data', $json );
		}

		/**
		 * Prints the Underscore.js templates.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function print_templates() {

			$m_templates = array();
			$s_templates = array();
			$c_templates = array(); ?>

			<script type="text/html" id="tmpl-butterbean-nav">
				<?php butterbean_get_nav_template(); ?>
			</script>

			<?php foreach ( $this->managers as $manager ) {

				if ( ! $manager->check_capabilities() )
					continue;

				if ( ! in_array( $manager->type, $m_templates ) ) {
					$m_templates[] = $manager->type;

					$manager->print_template();
				}

				foreach ( $manager->sections as $section ) {

					if ( ! in_array( $section->type, $s_templates ) ) {
						$s_templates[] = $section->type;

						$section->print_template();
					}
				}

				foreach ( $manager->controls as $control ) {

					if ( ! in_array( $control->type, $c_templates ) ) {
						$c_templates[] = $control->type;

						$control->print_template();
					}
				}
			}
		}

		/**
		 * Renders our Backbone views.  We're calling this late in the page load so
		 * that other scripts have an opportunity to extend with their own, custom
		 * views for custom controls and such.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function render_views() { ?>

			<script type="text/javascript">
				if (typeof butterbean !== 'undefined') {
					( function( api ) {
						if ( _.isObject( api ) && _.isFunction( api.render ) ) {
							api.render();
						}
					}( butterbean ) );
				}
			</script>
		<?php }

		/**
		 * Saves the settings.
		 *
		 * @since  1.0.0
		 * @access public
		 * @return void
		 */
		public function update( $post_id ) {

			$do_autosave = defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE;
			$is_autosave = wp_is_post_autosave( $post_id );
			$is_revision = wp_is_post_revision( $post_id );

			if ( $do_autosave || $is_autosave || $is_revision )
				return;

			foreach ( $this->managers as $manager ) {

				if ( $manager->check_capabilities() )
					$manager->save( $post_id );
			}
		}

		/**
		 * Helper method for sorting sections and controls by priority.
		 *
		 * @since  1.0.0
		 * @access protected
		 * @param  object     $a
		 * @param  object     $b
		 * @return int
		 */
		protected function priority_sort( $a, $b ) {

			if ( $a->priority === $b->priority )
				return $a->instance_number - $b->instance_number;

			return $a->priority - $b->priority;
		}
	}

	/**
	 * Gets the instance of the `ButterBean` class.  This function is useful for quickly grabbing data
	 * used throughout the plugin.
	 *
	 * @since  1.0.0
	 * @access public
	 * @return object
	 */
	function butterbean() {
		return ButterBean::get_instance();
	}

	// Let's do this thang!
	butterbean();
}
