<?php
/**
 * Facilitates storage and display of license key warning notices.
 *
 * @internal
 */
class Tribe__PUE__Notices {
	const INVALID_KEY = 'invalid_key';
	const UPGRADE_KEY = 'upgrade_key';
	const EXPIRED_KEY = 'expired_key';
	const STORE_KEY   = 'tribe_pue_key_notices';

	protected $registered = array();
	protected $saved_notices = array();
	protected $notices = array();

	/**
	 * Sets up license key related admin notices.
	 */
	public function __construct() {
		$this->populate();
		add_action( 'current_screen', array( $this, 'setup_notices' ) );
		add_action( 'tribe_pue_notices_save_notices', array( $this, 'maybe_undismiss_notices' ) );
	}

	/**
	 * Registers a plugin name that should be used in license key notifications.
	 *
	 * If, on a given request, the name is not registered then the plugin name will not
	 * feature in any notifications. The benefit is that if a plugin is suddenly removed,
	 * it's name can be automatically dropped from any pre-registered persistent
	 * notifications.
	 *
	 * @param string $plugin_name
	 */
	public function register_name( $plugin_name ) {
		$this->registered[] = $plugin_name;
	}

	/**
	 * Restores plugins added on previous requests to the relevant notification
	 * groups.
	 */
	protected function populate() {
		$this->saved_notices = (array) get_option( self::STORE_KEY, array() );

		if ( empty( $this->saved_notices ) ) {
			return;
		}

		$this->notices = array_merge_recursive( $this->notices, $this->saved_notices );

		// Cleanup
		foreach ( $this->notices as $key => &$plugin_lists ) {
			// Purge any elements that are not arrays
			if ( ! is_array( $plugin_lists ) ) {
				unset( $this->notices[ $key ] );
				continue;
			}
		}
	}

	/**
	 * Saves any license key notices already added.
	 */
	public function save_notices() {
		update_option( self::STORE_KEY, $this->notices );

		/**
		 * Fires after PUE license key notices have been saved.
		 *
		 * @param array $current_notices
		 * @param array $previously_saved_notices
		 */
		do_action( 'tribe_pue_notices_save_notices', $this->notices, $this->saved_notices );
	}

	/**
	 * Undismisses license key notifications where appropriate.
	 *
	 * The idea is that if an invalid key is detected for one or more plugins, we show a notification
	 * until a user dismisses it. That user will not then see the notification again unless or until
	 * an additional plugin name is added to the invalid key list.
	 *
	 * Example:
	 *
	 *     - Notification listing "Eventbrite" and "Pro" keys as invalid shows
	 *     - User X dismisses the notification
	 *     - The "Pro" license is fixed/corrected - notification remains in a "dismissed" status for User X
	 *     - "Filter Bar" is added to the list of invalid keys
	 *     - The invalid key notification is undismissed, to make all users (including User X) aware of
	 *       the problem re Filter Bar
	 */
	public function maybe_undismiss_notices() {
		foreach ( $this->notices as $notice_type => $plugin_list ) {
			if ( is_array( $this->saved_notices ) && ! empty( $this->saved_notices[ $notice_type ] ) ) {
				$new_plugins = array_diff_key( $this->notices[ $notice_type ], $this->saved_notices[ $notice_type ] );
			} else {
				$new_plugins = $this->notices[ $notice_type ];
			}

			if ( ! empty( $new_plugins ) ) {
				Tribe__Admin__Notices::instance()->undismiss_for_all( 'pue_key-' . $notice_type );
			}
		}
	}

	/**
	 * Used to include a plugin in a notification.
	 *
	 * For example, this could be used to add "My Plugin" to the expired license key
	 * notification by passing Tribe__PUE__Notices::EXPIRED_KEY as the second param.
	 *
	 * Plugins can only be added to one notification group at a time, so if a plugin
	 * was already added to the MISSING_KEY group and is subsequently added to the
	 * INVALID_KEY group, the previous entry (under MISSING_KEY) will be cleared.
	 *
	 * @param string $notice_type
	 * @param string $plugin_name
	 */
	public function add_notice( $notice_type, $plugin_name ) {
		$this->clear_notices( $plugin_name, true );
		$this->notices[ $notice_type ][ $plugin_name ] = true;
		$this->save_notices();
	}

	/**
	 * Returns whether or not a given plugin name has a specific notice
	 *
	 * @param string $plugin_name
	 * @param string|null $notice_type
	 *
	 * @return boolean
	 */
	public function has_notice( $plugin_name, $notice_type = null ) {
		if ( $notice_type ) {
			return ! empty( $this->notices[ $notice_type ][ $plugin_name ] );
		}

		foreach ( $this->notices as $notice_type => $plugins ) {
			if ( ! empty( $plugins[ $plugin_name ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Removes any notifications for the specified plugin.
	 *
	 * Useful when a valid license key is detected for a plugin, where previously
	 * it might have been included under a warning notification.
	 *
	 * If the optional second param is set to true then this change will not
	 * immediately be committed to storage (useful if we know this will happen in
	 * any case later on in the same request).
	 *
	 * @param string $plugin_name
	 * @param bool $defer_saving_change = false
	 */
	public function clear_notices( $plugin_name, $defer_saving_change = false ) {
		foreach ( $this->notices as $notice_type => &$list_of_plugins ) {
			unset( $list_of_plugins[ $plugin_name ] );
		}

		if ( ! $defer_saving_change ) {
			$this->save_notices();
		}
	}

	/**
	 * Tests to see if there are any extant notifications and renders them if so.
	 *
	 * This must run prior to Tribe__Admin__Notices::hook() (which currently runs during
	 * "current_screen" priority 20).
	 */
	public function setup_notices() {
		// Don't allow this to run multiple times
		remove_action( 'current_screen', array( $this, 'setup_notices' ) );

		// No need to display license key notices to users without appropriate capabilities
		if ( ! current_user_can( 'install_plugins' ) ) {
			return;
		}

		foreach ( $this->notices as $notice_type => $plugin_names ) {
			if ( empty( $plugin_names ) ) {
				continue;
			}

			$callback = array( $this, 'render_' . $notice_type );

			if ( is_callable( $callback ) ) {
				tribe_notice( 'pue_key-' . $notice_type, $callback, 'dismiss=1&type=warning' );
			}
		}
	}

	/**
	 * Generate a notice listing any plugins for which license keys have been entered but
	 * are invalid (in the sense of not matching PUE server records or having been revoked
	 * rather than having expired which is handled separately).
	 *
	 * In the context of the plugin admin screen, will not render if the key-has-expired
	 * notice is also scheduled to display.
	 */
	public function render_invalid_key() {
		global $pagenow;

		if ( 'plugins.php' === $pagenow && ! empty( $this->notices[ self::EXPIRED_KEY ] ) ) {
			return;
		}

		$plugin_names = $this->get_formatted_plugin_names( self::INVALID_KEY );

		if ( empty( $plugin_names ) ) {
			return;
		}

		$prompt = sprintf( _n(
			"It looks like you're using %s, but the license key you supplied does not appear to be valid or is missing. Please review and fix so that you can always have access to our latest versions!",
			"It looks like you're using %s, but the license keys you supplied do not appear to be valid or are missing. Please review and fix so that you can always have access to our latest versions!",
			count( $this->notices[ self::INVALID_KEY ] ),
			'tribe-common'
		),
			$plugin_names
		);

		$action_steps = $this->find_your_key_text();

		$this->render_notice( 'pue_key-' . self::INVALID_KEY, "<p>$prompt</p> <p>$action_steps</p>" );
	}

	/**
	 * Generate a notice listing any plugins for which license keys have expired.
	 *
	 * This notice should only appear at the top of the plugin admin screen and "trumps"
	 * the missing/invalid key notice on that screen only.
	 */
	public function render_expired_key() {
		global $pagenow;

		if ( 'plugins.php' !== $pagenow ) {
			return;
		}

		$plugin_names = $this->get_formatted_plugin_names( self::EXPIRED_KEY );

		if ( empty( $plugin_names ) ) {
			return;
		}

		$prompt = sprintf( _n(
				'There is an update available for %1$s but your license has expired. %2$sVisit the Events Calendar website to renew your license.%3$s',
				'Updates are available for %1$s but your license keys have expired. %2$sVisit the Events Calendar website to renew your licenses.%3$s',
				count( $this->notices[ self::EXPIRED_KEY ] ),
				'tribe-common'
			),
			$plugin_names,
			'<a href="http://m.tri.be/195d" target="_blank">',
			'</a>'
		);

		$renew_action =
			'<a href="http://m.tri.be/195y" target="_blank" class="button button-primary">' .
			__( 'Renew Your License Now', 'tribe-common' ) .
			'<span class="screen-reader-text">' .
			__( ' (opens in a new window)', 'tribe-common' ) .
			'</span></a>';

		$this->render_notice( 'pue_key-' . self::EXPIRED_KEY, "<p>$prompt</p> <p>$renew_action</p>" );
	}

	/**
	 * Generate a notice listing any plugins which have valid license keys, but those keys
	 * have met or exceeded the permitted number of installations they can be applied to.
	 */
	public function render_upgrade_key() {
		$plugin_names = $this->get_formatted_plugin_names( self::UPGRADE_KEY );

		if ( empty( $plugin_names ) ) {
			return;
		}

		$prompt = sprintf( _n(
				'You have entered a license key for %1$s but the key is out of installs. %2$sVisit the Events Calendar website%3$s to to manage your installs, upgrade your license, or purchase a new one.',
				'You have entered license keys for %1$s but your keys are out of installs. %2$sVisit the Events Calendar website%3$s to to manage your installs, upgrade your licenses, or purchase new ones.', count( $this->notices[ self::UPGRADE_KEY ] ),
				'tribe-common'
			),
			$plugin_names,
			'<a href="http://m.tri.be/195d" target="_blank">',
			'</a>'
		);

		$this->render_notice( 'pue_key-' . self::UPGRADE_KEY, "<p>$prompt</p>" );
	}

	/**
	 * Renders the notice itself (the provided HTML will be wrapped in a suitable container div).
	 *
	 * @param string $slug
	 * @param string $inner_html
	 */
	protected function render_notice( $slug, $inner_html ) {
		$spirit_animal = esc_url( Tribe__Main::instance()->plugin_url . 'src/resources/images/spirit-animal.png' );

		$html =
			'<div class="api-check">
				<div class="tribe-spirit-animal">
					<img src="' . $spirit_animal . '"/>
				</div>
				<div class="notice-content">' . $inner_html . '</div>
			</div>';

		Tribe__Admin__Notices::instance()->render( $slug, $html );
	}

	/**
	 * @return string
	 */
	protected function find_your_key_text() {
		return sprintf(
			__( 'You can find your license keys by logging in to %1$syour account on theeventscalendar.com%2$s and you can enter them over on the %3$ssettings page%2$s.', 'tribe-common' ),
			'<a href="http://m.tri.be/195d" target="_blank">',
			'</a>',
			'<a href="' . admin_url( 'edit.php?page=tribe-common&tab=licenses&post_type=tribe_events' ) . '">'
		);
	}

	/**
	 * Transforms the array referenced by group into a human readable,
	 * comma delimited list.
	 *
	 * Examples of output:
	 *
	 *     # One name
	 *     "Ticket Pro"
	 *
	 *     # Two names
	 *     "Ticket Pro and Calendar Legend"
	 *
	 *     # Three names
	 *     "Ticket Pro, Calendar Legend and Date Stars"
	 *
	 *     # Fallback
	 *     "Unknown Plugin(s)"
	 *
	 * @param string $group
	 *
	 * @return string
	 */
	protected function get_formatted_plugin_names( $group ) {
		if ( ! count( $this->notices[ $group ] ) ) {
			return '';
		}

		$plugin_list = array_intersect( $this->registered, array_keys( $this->notices[ $group ] ) );
		$num_plugins = count( $plugin_list );

		if ( 0 === $num_plugins ) {
			return '';
		} elseif ( 1 === $num_plugins ) {
			$html = current( $plugin_list );
		} elseif ( 1 < $num_plugins ) {
			$all_but_last = join( ', ', array_slice( $plugin_list, 0, count( $plugin_list ) - 1 ) );
			$last = current( array_slice( $plugin_list, count( $plugin_list ) - 1, 1 ) );
			$html = sprintf( _x( '%1$s and %2$s', 'formatted plugin list', 'tribe-common' ), $all_but_last, $last );
		}

		return '<span class="plugin-list">' . $html . '</span>';
	}
}
