<?php
/**
 * Gamajo Dashboard Glancer
 *
 * @package   Gamajo_Dashboard_Glancer
 * @author    Gary Jones
 * @link      http://gamajo.com/dashboard-glancer
 * @copyright 2013 Gary Jones, Gamajo Tech
 * @license   GPL-2.0+
 * @version   1.0.0
 */

/**
 * Easily add items to the At a Glance Dashboard widget in WordPress 3.8+.
 *
 * @package Gamajo_Dashboard_Glancer
 * @author  Gary Jones
 */
class Gamajo_Dashboard_Glancer {

	/**
	 * Hold all of the items to show.
	 *
	 * @since 1.0.0
	 *
	 * @type array
	 */
	protected $items;

	/**
	 * Automatically show any registered items.
	 *
	 * With this, there's no need to explicitly call show() during the dashboard_glance_items hook,
	 * and items can be registered at any time before dashboard_glance_items priority 20 (including on earlier hooks).
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		add_action( 'dashboard_glance_items', array( $this, 'show' ), 20 );
	}

	/**
	 * Register one or more post type items to be shown on the dashboard widget.
	 *
	 * @since 1.0.0
	 *
	 * @param array|string $post_types Post type name, or array of post type names.
	 * @param array|string $statuses   Post status or array of different post type statuses.
	 *
	 * @return Return early if action hook has already passed, or no valid post types were given.
	 */
	public function add( $post_types, $statuses = 'publish' ) {
		// If relevant output action hook has already passed, then no point in proceeding.
		if ( did_action( 'dashboard_glance_items' ) ) {
			_doing_it_wrong( __CLASS__, 'Trying to add At a Glance items to dashboard widget afterhook already fired', '1.0.0' );
			return;
		}

		$post_types = $this->unset_invalid_post_types( (array) $post_types );

		// If all given post types were invalid, bail now.
		if ( ! $post_types ) {
			return;
		}

		// Register each combination of given post type and status.
		foreach ( $post_types as $post_type ) {
			foreach ( (array) $statuses as $status ) {
				$this->items[] = array(
					'type'   => $post_type,
					'status' => $status, // No checks yet to see if status is valid.
				);
			}
		}
	}

	/**
	 * Show the items on the dashboard widget.
	 *
	 * @since 1.0.0
	 */
	public function show() {
		foreach ( $this->items as $item ) {
			echo $this->get_single_item( $item );
		}
		// Reset items, so items aren't shown again if show() is re-called.
		unset( $this->items );
	}

	/**
	 * Check one or more post types to see if they are valid.
	 *
	 * @since 1.0.0
	 *
	 * @param array $post_types Each of the post types to check.
	 *
	 * @return array List of the given post types that are valid.
	 */
	protected function unset_invalid_post_types( array $post_types ) {
		foreach ( $post_types as $index => $post_type ) {
			$post_type_object = get_post_type_object( $post_type );
			if ( is_null( $post_type_object ) ) {
				unset( $post_types[ $index ] );
			}
		}

		return $post_types;
	}

	/**
	 * Build and return the data and markup for a single item.
	 *
	 * If the item count is zero, return an empty string, to avoid visual clutter.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $item Registered item.
	 *
	 * @return string Markup, or empty string if item count is zero.
	 */
	protected function get_single_item( array $item ) {
		$num_posts = wp_count_posts( $item['type'] );
		$status    = $item['status'];
		$count     = (int) $num_posts->$status;

		if ( ! $count ) {
			return '';
		}

		$href = $this->get_link_url( $item );
		$text = number_format_i18n( $count ) . ' ' . $this->get_label( $item, $count );
		$text = $this->maybe_link( $text, $href );

		return $this->get_markup( $text, $item['type'] );
	}

	/**
	 * Get the singular or plural label for an item.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $item  Registered item.
	 * @param  int   $count Number of items present in WP.
	 *
	 * @return string
	 */
	protected function get_label( array $item, $count ) {
		$post_type_object = get_post_type_object( $item['type'] );
		$label            = 1 === $count ? $post_type_object->labels->singular_name : $post_type_object->labels->name;

		// Append status for non-publish statuses for disambiguation.
		if ( 'publish' !== $item['status'] ) {
			$label .= ' (' . $item['status'] . ')';
		}

		return $label;
	}

	/**
	 * Build the URL that linked items use.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $item Registered item.
	 *
	 * @return string      Admin URL to view the entries of the given post type with the given status
	 */
	public function get_link_url( array $item ) {
		return 'edit.php?post_status=' . $item['status'] . '&amp;post_type=' . $item['type'];
	}

	/**
	 * Wrap a glance item in a link, if the current user can edit posts.
	 *
	 * @since 1.0.0
	 *
	 * @param string $text Text to potentially wrap in a link.
	 * @param string $href Link target.
	 *
	 * @return string       Text wrapped in a link if current user can edit posts, or original text otherwise.
	 */
	protected function maybe_link( $text, $href ) {
		if ( current_user_can( 'edit_posts' ) ) {
			return '<a href="' . esc_url( $href ) . '">' . $text . '</a>';
		}
		return $text;
	}

	/**
	 * Wrap number and text within list item markup.
	 *
	 * @since 1.0.0
	 *
	 * @param string $text  Text to display. May be wrapped in a link.
	 * @param string $post_type Post Type.
	 */
	protected function get_markup( $text, $post_type ) {
		return '<li class="' . sanitize_html_class( $post_type . '-count' ) . '">' . $text . '</li>' . "\n";
	}

}
