<?php
/**
 * An extension for Connections Business Directory which adds advanced features to the categories.
 *
 * @package   Connections Business Directory Extension - Enhanced Categories
 * @category  Extension
 * @author    Steven A. Zahm
 * @license   GPL-2.0+
 * @link      https://connections-pro.com
 * @copyright 2023 Steven A. Zahm
 *
 * @wordpress-plugin
 * Plugin Name:       Connections Business Directory Extension - Enhanced Categories
 * Plugin URI:        https://connections-pro.com/add-on/enhanced-categories/
 * Description:       An extension for Connections Business Directory which adds advanced features to the categories.
 * Version:           1.1
 * Author:            Steven A. Zahm
 * Author URI:        https://connections-pro.com
 * License:           GPL-2.0+
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain:       connections-enhanced-categories
 * Domain Path:       /languages
 */

use Connections_Directory\Utility\_parse;

if ( ! class_exists( 'Connections_Categories' ) ) {

	final class Connections_Categories {

		const VERSION = '1.1';

		/**
		 * @var Connections_Categories Stores the instance of this class.
		 *
		 * @since 1.0
		 */
		private static $instance;

		/**
		 * @var string The absolute path this file.
		 *
		 * @since 1.0
		 */
		private static $file = '';

		/**
		 * @var string The URL to the plugin's folder.
		 *
		 * @since 1.0
		 */
		private static $url = '';

		/**
		 * @var string The absolute path to this plugin's folder.
		 *
		 * @since 1.0
		 */
		private static $path = '';

		/**
		 * @var string The basename of the plugin.
		 *
		 * @since 1.0
		 */
		private static $basename = '';

		/**
		 * A dummy constructor to prevent the class from being loaded more than once.
		 *
		 * @since 1.0
		 */
		public function __construct() { /* Do nothing here */ }

		/**
		 * @since 1.0
		 *
		 * @return Connections_Categories
		 */
		public static function instance() {

			if ( ! isset( self::$instance ) && ! ( self::$instance instanceof Connections_Categories ) ) {

				self::$file     = __FILE__;
				self::$url      = plugin_dir_url( self::$file );
				self::$path     = plugin_dir_path( self::$file );
				self::$basename = plugin_basename( self::$file );

				/**
				 * This should run on the `plugins_loaded` action hook. Since the extension loads on the
				 * `plugins_loaded` action hook, load immediately.
				 */
				cnText_Domain::register(
					'connections-enhanced-categories',
					self::$basename,
					'load'
				);

				self::$instance = new Connections_Categories();

				if ( is_admin() ) {

					include dirname( __FILE__ ) . '/includes/class.term-image.php';
					new cnTerm_Image( __FILE__ );
				}

				self::hooks();

				// Activation/Deactivation hooks.
				register_activation_hook( dirname( __FILE__ ) . '/connections-categories.php', array( __CLASS__, 'activate' ) );
				register_deactivation_hook( dirname( __FILE__ ) . '/connections-categories.php', array( __CLASS__, 'deactivate' ) );

				// License and Updater.
				if ( class_exists( 'cnLicense' ) ) {

					new cnLicense( __FILE__, 'Enhanced Categories', self::VERSION, 'Steven A. Zahm' );
				}
			}

			return self::$instance;
		}

		/**
		 * Register all the hooks that makes this thing run.
		 *
		 * @since 1.0
		 */
		private static function hooks() {

			// Add the rewrite rules to support pretty permalinks.
			add_filter( 'cn_root_rewrite_rule-view', array( __CLASS__, 'addRootRewriteRules' ), 10, 2 );
			add_filter( 'cn_page_rewrite_rule-view', array( __CLASS__, 'addPageRewriteRules' ) );
			add_filter( 'cn_cpt_rewrite_rule-view', array( __CLASS__, 'addCPTRewriteRules' ), 10, 2 );

			// Register the settings.
			add_filter( 'cn_register_settings_sections', array( __CLASS__, 'registerSettingsSections' ) );
			add_filter( 'cn_register_settings_fields', array( __CLASS__, 'registerSettingsFields' ) );

			// These filters must be added before the `cn_register_settings_fields` filter.
			add_filter( 'cn_list_action_options', array( __CLASS__, 'listActionsOption' ) );

			// Render the "View Categories" list action link.
			add_action( 'cn_list_action-category_view', array( __CLASS__, 'viewLink' ) );

			// Add support to cnURL::permalink() for the `categories` permalink type.
			add_filter( 'cn_permalink-categories', array( __CLASS__, 'permalink' ), 10, 2 );

			// Display the categories on the view categories page.
			add_action( 'cn_view_categories', array( __CLASS__, 'viewPage' ), 10, 3 );

			// Display the categories before the results list and remove the filters after they are displayed,
			// so they do not affect other areas the filters might run. For example the Category Widget.
			add_action( 'cn_action_list_before', array( __CLASS__, 'addHooksBeforeList' ), -99 );
			add_action( 'cn_view_categories', array( __CLASS__, 'addHooksViewAll' ), -99 );
			add_action( 'cn_action_list_before', array( __CLASS__, 'viewBefore' ), 9 ); // Priority 9 so it displays before the category desc.
			add_action( 'cn_action_list_after', array( __CLASS__, 'removeHooksBeforeList' ), 99 );

			// Make the categories names permalinks.
			// add_filter( 'cn_entry_output_category_item', array( __CLASS__, 'categoryItem' ), 10, 6 );
			add_filter( 'cn_output_default_atts_category', array( __CLASS__, 'categoryDefaultAtts' ) );
			add_filter( 'cn_output_atts_category', array( __CLASS__, 'categoryAtts' ) );

			// Register image size.
			add_action( 'after_setup_theme', array( __CLASS__, 'registerImageSizes' ) );
			add_filter( 'image_size_names_choose', array( __CLASS__, 'registerImageSizeName' ) );

			// Used only for testing, will use lorem images as placeholders for categories which have no images attached.
			// add_filter( 'cnec_placeholder_html', array( __CLASS__, 'loremImage'), 10, 3 );

			// Register the shortcode.
			add_shortcode( 'connections_categories', array( __CLASS__, 'shortcode' ) );

			// Register the permitted shortcode options with their default values.
			add_filter( 'cn_list_atts_permitted', array( __CLASS__, 'registerShortcodeOptions' ) );

			// Prepare the parsed shortcode option values.
			add_filter( 'cn_list_atts', array( __CLASS__, 'prepareShortcodeOptions' ) );

			// Register the scripts.
			add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueueScripts' ) );

			// Add an action to purge widget fragment caches after adding/editing and entry.
			add_action( 'cn_clean_entry_cache', array( __CLASS__, 'clearFragments' ) );
			add_action( 'cn_clean_term_cache', array( __CLASS__, 'clearFragments' ) );
		}

		/**
		 * Callback for the `cn_view_categories` filter.
		 *
		 * Add the necessary hooks when viewing the "View All Categories" page.
		 *
		 * @internal
		 * @since 1.0
		 */
		public static function addHooksViewAll() {

			// Add the list class name.
			add_filter( 'cn_term_list_class', array( __CLASS__, 'categoryListClass_ViewAll' ), 10, 3 );
		}

		/**
		 * Callback for the `cn_action_list_before` filter.
		 *
		 * Add the necessary hooks when viewing the directory page.
		 *
		 * @internal
		 * @since 1.0
		 */
		public static function addHooksBeforeList() {

			if ( is_admin() ) {

				return;
			}

			// Display the categories before the results list.
			// add_action( 'cn_action_list_before', array( __CLASS__, 'viewBefore' ), 9 );

			// Add the list class name.
			add_filter( 'cn_term_list_class', array( __CLASS__, 'categoryListClass' ), 10, 3 );

			// Add the list item class name.
			add_filter( 'cn_term_list_item_class', array( __CLASS__, 'categoryListItemClass' ), 10, 4 );

			// Show category images.
			add_filter( 'cn_term_list_item', array( __CLASS__, 'categoryListItem' ), 10, 4 );
		}

		/**
		 * Callback for the `cn_action_list_after` filter.
		 *
		 * Remove the hook after the directory is rendered in order not to affect other instances when the categories
		 * can be rendered; i.e. the Category Widget in the Widget Pack.
		 *
		 * @internal
		 * @since 1.0
		 */
		public static function removeHooksBeforeList() {

			if ( is_admin() ) {

				return;
			}

			remove_action( 'cn_action_list_before', array( __CLASS__, 'viewBefore' ), 9 );
			remove_filter( 'cn_term_list_class', array( __CLASS__, 'categoryListClass' ), 10 );
			remove_filter( 'cn_term_list_item_class', array( __CLASS__, 'categoryListItemClass' ), 10 );
			remove_filter( 'cn_term_list_item', array( __CLASS__, 'categoryListItem' ), 10 );
		}

		/**
		 * Activation hook callback.
		 *
		 * @internal
		 * @since 1.0
		 */
		public static function activate() {

			/*
			 * This option is added for a check that will force a flush_rewrite() in connectionsLoad::adminInit() once.
			 * Should save the user from having to "save" the permalink settings.
			 */
			update_option( 'connections_flush_rewrite', '1' );
		}

		/**
		 * Deactivation hook callback.
		 *
		 * @internal
		 * @since 1.0
		 * @static
		 */
		public function deactivate() {

			// Flush so they are rebuilt.
			flush_rewrite_rules();
		}

		/**
		 * Callback for the `cn_root_rewrite_rule-view` filter.
		 *
		 * Add the root rewrite rules.
		 *
		 * NOTE: Using a filter, so I can add the rules right after the default root rules.
		 * This *should* prevent any rule conflicts.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $rules
		 * @param int   $pageID
		 *
		 * @return array
		 */
		public static function addRootRewriteRules( $rules, $pageID ) {

			$rules['view/categories/?$']
				= 'index.php?&page_id=' . $pageID . '&cn-view=categories';

			return $rules;
		}

		/**
		 * Callback for the `cn_page_rewrite_rule-view` filter.
		 *
		 * Add the page rewrite rules.
		 *
		 * NOTE: Using a filter, so I can add the rules right before the default page rules.
		 * This *should* prevent any rule conflicts.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $rules
		 *
		 * @return array
		 */
		public static function addPageRewriteRules( $rules ) {

			$rules['(.?.+?)/view/categories/?$']
				= 'index.php?pagename=$matches[1]&cn-view=categories';

			return $rules;
		}

		/**
		 * Callback for the `cn_cpt_rewrite_rule-view` filter.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array  $rules The rewrite rules array.
		 * @param string $slug  The CPT slug.
		 *
		 * @return array
		 */
		public static function addCPTRewriteRules( $rules, $slug ) {

			// View all entries.
			$rules[ $slug . '/(.+?)/view/categories/?$' ]
				= 'index.php?' . $slug . '=$matches[1]&cn-view=categories';

			return $rules;
		}

		/**
		 * Callback for the `cn_register_settings_sections` filter.
		 *
		 * Add the settings sections.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $sections
		 *
		 * @return array
		 */
		public static function registerSettingsSections( $sections ) {

			$settings = 'connections_page_connections_settings';

			$sections[] = array(
				'plugin_id' => 'connections_categories',
				'tab'       => 'display',
				'id'        => 'general',
				'position'  => 18,
				'title'     => esc_html__( 'Categories', 'connections-enhanced-categories' ),
				'callback'  => function() {
					esc_html_e(
						'The following settings are used to configure the category display before the results list.',
						'connections-enhanced-categories'
					);
				},
				'page_hook' => $settings,
			);

			$sections[] = array(
				'plugin_id' => 'connections_categories',
				'tab'       => 'display',
				'id'        => 'entry',
				'position'  => 18.1,
				'title'     => '',
				'callback'  => function() {
					esc_html_e(
						'The following settings are used to configure the display of the categories assigned to an entry.',
						'connections-enhanced-categories'
					);
				},
				'page_hook' => $settings,
			);

			return $sections;
		}

		/**
		 * Callback for the `cn_register_settings_fields` filter.
		 *
		 * Add the settings fields to the sections.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $fields
		 *
		 * @return array
		 */
		public static function registerSettingsFields( $fields ) {

			$settings = 'connections_page_connections_settings';

			$fields[] = array(
				'plugin_id'         => 'connections_categories',
				'id'                => 'breadcrumb',
				'position'          => 5,
				'page_hook'         => $settings,
				'tab'               => 'display',
				'section'           => 'general',
				'title'             => esc_html__( 'Breadcrumb', 'connections-enhanced-categories' ),
				'desc'              => esc_html__( 'Display the category breadcrumb above the results list?', 'connections-enhanced-categories' ),
				'type'              => 'checkbox',
				'default'           => 1,
				// This only is required once per settings section since each section is saved as an array.
				'sanitize_callback' => array( __CLASS__, 'sanitizeListOptions' ),
			);

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'display',
				'position'  => 10,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'general',
				'title'     => esc_html__( 'Display', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'Display the categories above the results list?', 'connections-enhanced-categories' ),
				'type'      => 'checkbox',
				'default'   => 1,
			);

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'depth',
				'position'  => 20,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'general',
				'title'     => esc_html__( 'Depth', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'The number of levels of the hierarchy to display. Enter `0` for unlimited (default). Enter `1` to display the parent categories only.', 'connections-enhanced-categories' ),
				'type'      => 'number',
				'size'      => 'small',
				'default'   => 0,
			);

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'display_children',
				'position'  => 30,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'general',
				'title'     => esc_html__( 'Display Children', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'Display only the children categories of the parent category when viewing a category page? Disabling this option will display all categories, to the set depth, regardless of the current category being viewed.', 'connections-enhanced-categories' ),
				'type'      => 'checkbox',
				'default'   => 1,
			);

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'display_count',
				'position'  => 40,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'general',
				'title'     => esc_html__( 'Display Count', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'Display the category counts? This is the number of directory entries assigned to a category.', 'connections-enhanced-categories' ),
				'type'      => 'checkbox',
				'default'   => 0,
			);

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'hide_empty',
				'position'  => 50,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'general',
				'title'     => esc_html__( 'Hide Empty', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'Hide the empty categories? Hide the categories which have no entries assigned to them. If a child category of a parent category has been assigned to an entry the parent category will be displayed regardless if any entries have been assigned to the parent category.', 'connections-enhanced-categories' ),
				'type'      => 'checkbox',
				'default'   => 1,
			);

			// $fields[] = array(
			// 	'plugin_id' => 'connections_categories',
			// 	'id'        => 'show_image',
			// 	'position'  => 60,
			// 	'page_hook' => $settings,
			// 	'tab'       => 'display',
			// 	'section'   => 'general',
			// 	'title'     => esc_html__( 'Display Image', 'connections-enhanced-categories' ),
			// 	'desc'      => esc_html__( 'Display the category image.', 'connections-enhanced-categories' ),
			// 	'type'      => 'checkbox',
			// 	'default'   => 0,
			// );

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'display_style',
				'position'  => 60,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'general',
				'title'     => esc_html__( 'Display Style', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'The style used to display the categories before the results list. NOTE: When displaying with the Image Grid option, the category depth is limited to 1, displaying the parent categories only.', 'connections-enhanced-categories' ),
				'type'      => 'select',
				'options'   => array(
					'list' => esc_html__( 'List', 'connections-enhanced-categories' ),
					'grid' => esc_html__( 'Image Grid', 'connections-enhanced-categories' ),
				),
				'default'   => 'list',
			);

			// $fields[] = array(
			// 	'plugin_id' => 'connections_categories',
			// 	'id'        => 'image_size',
			// 	'position'  => 70,
			// 	'page_hook' => $settings,
			// 	'tab'       => 'display',
			// 	'section'   => 'general',
			// 	'title'     => esc_html__( 'Image Size', 'connections-enhanced-categories' ),
			// 	'desc'      => esc_html__( 'The image size to use when displaying categories images.', 'connections-enhanced-categories' ),
			// 	'type'      => 'select',
			// 	'options'   => self::getImageSizeOptions(),
			// 	'default'   => 'thumbnail',
			// );

			$fields[] = array(
				'plugin_id'         => 'connections_categories',
				'id'                => 'permalink_enabled',
				'position'          => 10,
				'page_hook'         => $settings,
				'tab'               => 'display',
				'section'           => 'entry',
				'title'             => esc_html__( 'Permalinks Enabled', 'connections-enhanced-categories' ),
				'desc'              => esc_html__( 'Make the categories names in the entry details permalinks?', 'connections-enhanced-categories' ),
				'type'              => 'checkbox',
				'default'           => 1,
				'sanitize_callback' => array( __CLASS__, 'sanitizeEntryOptions' ), // This only is required once per settings section since each section is saved as an array.
			);

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'display_hierarchy',
				'position'  => 20,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'entry',
				'title'     => esc_html__( 'Display Parents', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'Display the category hierarchy?', 'connections-enhanced-categories' ),
				'type'      => 'checkbox',
				'default'   => 0,
			);

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'display_style',
				'position'  => 30,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'entry',
				'title'     => esc_html__( 'Display Style', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'Display the categories in the entry details as a block or list?', 'connections-enhanced-categories' ),
				'type'      => 'select',
				'options'   => array(
					'block' => esc_html__( 'Block', 'connections-enhanced-categories' ),
					'list'  => esc_html__( 'List', 'connections-enhanced-categories' ),
				),
				'default'   => 'block',
			);

			// The templates have options for changing this string.
			// $fields[] = array(
			// 	'plugin_id' => 'connections_categories',
			// 	'id'        => 'label',
			// 	'position'  => 40,
			// 	'page_hook' => $settings,
			// 	'tab'       => 'display',
			// 	'section'   => 'entry',
			// 	'title'     => esc_html__( 'Label', 'connections-enhanced-categories' ),
			// 	'desc'      => esc_html__( 'The label to display before the categories assigned to the entry.', 'connections-enhanced-categories' ),
			// 	'type'      => 'text',
			// 	'size'      => 'medium',
			// 	'default'   => esc_html__( 'Categories:', 'connections-enhanced-categories' ) . ' ',
			// );

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'separator',
				'position'  => 40,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'entry',
				'title'     => esc_html__( 'Separator', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'The separator to be used to separate the categories assigned to an entry.', 'connections-enhanced-categories' ),
				'type'      => 'text',
				'size'      => 'small',
				'default'   => ', ',
			);

			$fields[] = array(
				'plugin_id' => 'connections_categories',
				'id'        => 'parent_separator',
				'position'  => 50,
				'page_hook' => $settings,
				'tab'       => 'display',
				'section'   => 'entry',
				'title'     => esc_html__( 'Parent Separator', 'connections-enhanced-categories' ),
				'desc'      => esc_html__( 'The separator to be used to separate the categories when displaying the category hierarchy.', 'connections-enhanced-categories' ),
				'type'      => 'text',
				'size'      => 'small',
				'default'   => ' &raquo; ',
			);

			return $fields;
		}

		/**
		 * Sanitize the settings for the category list option on save.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $options
		 *
		 * @return array
		 */
		public static function sanitizeListOptions( $options ) {

			$defaults = array(
				'breadcrumb'       => isset( $options['breadcrumb'] ) ? 1 : 0,
				'display'          => isset( $options['display'] ) ? 1 : 0,
				'depth'            => 0,
				'display_children' => isset( $options['display_children'] ) ? 1 : 0,
				'display_count'    => isset( $options['display_count'] ) ? 1 : 0,
				'hide_empty'       => isset( $options['hide_empty'] ) ? 1 : 0,
				// 'show_image'       => isset( $options['show_image'] ) ? 1 : 0,
				'display_style'    => 'list',
				// 'image_size'       => 'thumbnail',
			);

			$options = cnSanitize::args( $options, $defaults );

			$options['breadcrumb']       = intval( $options['breadcrumb'] );
			$options['display']          = intval( $options['display'] );
			$options['depth']            = intval( $options['depth'] );
			$options['display_children'] = intval( $options['display_children'] );
			$options['display_count']    = intval( $options['display_count'] );
			$options['hide_empty']       = intval( $options['hide_empty'] );
			// $options['show_image']       = intval( $options['show_image'] );
			$options['display_style']    = in_array( $options['display_style'], array( 'list', 'grid' ) ) ? $options['display_style'] : 'list';
			// $options['image_size']       = array_key_exists( $options['image_size'], self::getImageSizes() ) ? $options['image_size'] : 'thumbnail';

			// If the display style is set to image grid then the category depth needs to be 1 so only the parent
			// categories are displayed.
			if ( 'grid' === $options['display_style'] ) {

				$options['depth'] = 1;
			}

			// Ensure the fragment cache is cleared when the settings are saved.
			self::clearFragments();

			return $options;
		}

		/**
		 * Sanitize the settings for the category display option on save.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $options
		 *
		 * @return array
		 */
		public static function sanitizeEntryOptions( $options ) {

			$defaults = array(
				'permalink_enabled' => isset( $options['permalink_enabled'] ) ? 1 : 0,
				'display_hierarchy' => isset( $options['display_hierarchy'] ) ? 1 : 0,
				'display_style'     => 'block',
				// 'label'             => esc_html__( 'Categories:', 'connections-enhanced-categories' ) . ' ',
				'separator'         => ', ',
				'parent_separator'  => ' &raquo; ',
			);

			$options = cnSanitize::args( $options, $defaults );

			$options['permalink_enabled'] = intval( $options['permalink_enabled'] );
			$options['display_hierarchy'] = intval( $options['display_hierarchy'] );
			$options['display_style']     = ! in_array( $options['display_style'], array( 'block', 'list' ) ) ? 'block' : $options['display_style'];
			// $options['label']             = esc_html( $options['label'] );
			$options['separator']         = esc_html( $options['separator'] );
			$options['parent_separator']  = esc_html( $options['parent_separator'] );

			// Ensure the fragment cache is cleared when the settings are saved.
			self::clearFragments();

			return $options;
		}

		/**
		 * Callback for the `after_setup_theme` action.
		 *
		 * Register the image thumbnail size.
		 *
		 * @internal
		 * @since 1.0
		 */
		public static function registerImageSizes() {

			add_image_size( 'cn-category-thumb', 250, 250, true );
		}

		/**
		 * Callback for the `image_size_names_choose` filter.
		 *
		 * This will allow WordPress to display the "nice name" for the registered image size.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $sizes
		 *
		 * @return array
		 */
		public static function registerImageSizeName( $sizes ) {

			return array_merge(
				$sizes,
				array(
					'cn-category-thumb' => esc_html__( 'Connections Category Thumbnail', 'connections-enhanced-categories' ),
				)
			);
		}

		/**
		 * @link https://codex.wordpress.org/Function_Reference/get_intermediate_image_sizes#Examples
		 *
		 * @access private
		 * @since 1.0
		 *
		 * @return array
		 */
		public static function getImageSizes() {

			global $_wp_additional_image_sizes;

			$sizes = array();

			foreach ( get_intermediate_image_sizes() as $_size ) {

				if ( in_array( $_size, array( 'thumbnail', 'medium', 'medium_large', 'large' ) ) ) {

					$sizes[ $_size ]['width']  = get_option( "{$_size}_size_w" );
					$sizes[ $_size ]['height'] = get_option( "{$_size}_size_h" );
					$sizes[ $_size ]['crop']   = (bool) get_option( "{$_size}_crop" );

				} elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) {

					$sizes[ $_size ] = array(
						'width'  => $_wp_additional_image_sizes[ $_size ]['width'],
						'height' => $_wp_additional_image_sizes[ $_size ]['height'],
						'crop'   => $_wp_additional_image_sizes[ $_size ]['crop'],
					);
				}
			}

			return $sizes;
		}

		/**
		 * @link https://codex.wordpress.org/Function_Reference/get_intermediate_image_sizes#Examples
		 *
		 * @access private
		 * @since 1.0
		 *
		 * @param string $size The registered image size.
		 *
		 * @return false|array
		 */
		public static function getImageSize( $size ) {

			$sizes = self::getImageSizes();

			if ( isset( $sizes[ $size ] ) ) {

				return $sizes[ $size ];
			}

			return false;
		}

		/**
		 * @access private
		 * @since 1.0
		 *
		 * @return array
		 */
		private static function getImageSizeOptions() {

			$options = array();

			// This filter is documented in wp-admin/includes/media.php.
			$names = apply_filters(
				'image_size_names_choose',
				array(
					'thumbnail' => esc_html__( 'Thumbnail', 'connections-enhanced-categories' ),
					'medium'    => esc_html__( 'Medium', 'connections-enhanced-categories' ),
					'large'     => esc_html__( 'Large', 'connections-enhanced-categories' ),
					'full'      => esc_html__( 'Full Size', 'connections-enhanced-categories' ),
				)
			);

			$sizes = self::getImageSizes();

			foreach ( $sizes as $key => $size ) {

				if ( isset( $names[ $key ] ) ) {

					$options[ $key ] = $names[ $key ] . ' ' . $size['width'] . '&times;' . $size['height'];

				} else {

					// Make the key name more reader friendly.
					$name = str_replace( array( '-', '_' ), ' ', $key );
					$name = ucwords( $name );

					$options[ $key ] = $name . ' ' . $size['width'] . '&times;' . $size['height'];
				}
			}

			return $options;
		}

		/**
		 * Callback for the `cn_list_action_options` filter.
		 *
		 * Add the settings option to the List Actions options.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $actions The actions attributes array.
		 *
		 * @return array
		 */
		public static function listActionsOption( $actions ) {

			$actions['category_view'] = __(
				'When this option is enabled a "View All Categories" link will be displayed.',
				'connections-enhanced-categories'
			);

			return $actions;
		}

		/**
		 * Callback for the `wp_enqueue_scripts` action.
		 *
		 * Enqueue the scripts.
		 *
		 * @internal
		 * @since 1.0
		 */
		public static function enqueueScripts() {

			// If SCRIPT_DEBUG is set and TRUE load the non-minified JS files, otherwise, load the minified files.
			$min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
			$url = cnURL::makeProtocolRelative( self::$url );

			switch ( cnSettingsAPI::get( 'connections_categories', 'general', 'display_style' ) ) {

				case 'grid':
					if ( 'categories' === cnQuery::getVar( 'cn-view' ) ) {

						wp_enqueue_style( 'cn-category-list', $url . "assets/css/cn-category-list$min.css" );

					} else {

						wp_enqueue_style( 'cn-category-images', $url . "assets/css/cn-category-images$min.css" );
					}

					break;

				default:
					wp_enqueue_style( 'cn-category-list', $url . "assets/css/cn-category-list$min.css" );
			}
		}

		/**
		 * Callback for the `cn_list_action-category_view` action.
		 *
		 * The callback that is run to output the "View Categories" link.
		 * The action being run is cn_list_action-{$slug} called from cnTemplatePart::listActions().
		 * $slug is the array key set in the $actions array in self::listActionsOption().
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $atts
		 */
		public static function viewLink( $atts = array() ) {

			$defaults = array(
				'text' => esc_html__( 'View All Categories', 'connections-enhanced-categories' ),
			);

			$atts = wp_parse_args( $atts, $defaults );

			$url = cnURL::permalink(
				array(
					'type'    => 'categories',
					'home_id' => $atts['home_id'],
					'return'  => true,
					'data'    => 'url',
				)
			);

			printf( '<a href="%1$s" title="%2$s">%2$s</a>', esc_url( $url ), esc_html( $atts['text'] ) );
		}

		/**
		 * Callback for `cn_permalink-categories` filter.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param string $permalink
		 * @param array  $atts
		 *
		 * @return string
		 */
		public static function permalink( $permalink, $atts ) {

			/** @var $wp_rewrite WP_Rewrite */
			global $wp_rewrite;

			if ( 'categories' == $atts['type'] ) {

				if ( $wp_rewrite->using_permalinks() ) {

					$permalink = trailingslashit( $permalink . 'view/categories/' );

				} else {

					$permalink = add_query_arg( array( 'cn-view' => 'categories' ), $permalink );
				}
			}

			return $permalink;
		}

		/**
		 * The category list default options.
		 *
		 * @access private
		 * @since  1.0
		 *
		 * @return array
		 */
		private static function defaults() {

			return array(
				// 'type'             => 'link',
				'show_option_none' => '',
				'hide_empty'       => 1 == cnSettingsAPI::get( 'connections_categories', 'general', 'hide_empty' ) ? true : false,
				'show_count'       => cnSettingsAPI::get( 'connections_categories', 'general', 'display_count' ),
				'depth'            => cnSettingsAPI::get( 'connections_categories', 'general', 'depth' ),
				'parent_id'        => array(),
				'child_of'         => 0,
				'exclude'          => array(),
				// 'layout'           => 'list',
				// 'columns'          => 3,
				'force_home'       => false,
				'home_id'          => cnSettingsAPI::get( 'connections', 'connections_home_page', 'page_id' ),

				// Support related core shortcode options.
				'category'         => array(),
				// 'exclude_category' => '',

				// Support related template shortcode options.
				// 'enable_category_by_root_parent' => FALSE,
			);
		}

		/**
		 * Callback for the `cn_list_atts_permitted` filter.
		 *
		 * Registers the permitted shortcode options and their default values.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $permitted
		 *
		 * @return array
		 */
		public static function registerShortcodeOptions( $permitted ) {

			$permitted['cnec_display_categories'] = cnSettingsAPI::get( 'connections_categories', 'general', 'display' );
			$permitted['cnec_display_breadcrumb'] = cnSettingsAPI::get( 'connections_categories', 'general', 'breadcrumb' );

			return $permitted;
		}

		/**
		 * Callback for the `cn_list_atts` filter.
		 *
		 * Prepares the user supplied values for the shortcode options.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $options
		 *
		 * @return array
		 */
		public static function prepareShortcodeOptions( $options ) {

			cnFormatting::toBoolean( $options['cnec_display_categories'] );
			cnFormatting::toBoolean( $options['cnec_display_breadcrumb'] );

			return $options;
		}

		/**
		 * The callback for the shortcode.
		 *
		 * @access public
		 * @since 1.0
		 *
		 * @param array  $atts
		 * @param null   $content
		 * @param string $tag
		 *
		 * @return mixed
		 */
		public static function shortcode( $atts = array(), $content = null, $tag = 'connections_categories' ) {

			$defaults = self::defaults();

			$atts = shortcode_atts( $defaults, $atts, $tag );

			// When in a shortcode, all output has to be returned.
			$atts['return'] = true;

			self::addHooksBeforeList();

			// return cnTemplatePart::category( $atts );
			$html = cnTemplatePart::walker( 'term-list', $atts );

			self::removeHooksBeforeList();

			return $html;
		}

		/**
		 * Callback for the `cn_view_categories` action.
		 *
		 * Display the category list on the "View All Categories" landing page.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array  $atts
		 * @param null   $content
		 * @param string $tag
		 */
		public static function viewPage( $atts, $content = null, $tag = 'connections_categories' ) {

			$defaults = self::defaults();

			$atts = cnSanitize::args( $atts, $defaults );

			// Depth should be unlimited when viewing all.
			if ( 'categories' === cnQuery::getVar( 'cn-view' ) ) {

				$atts['depth'] = 0;
			}

			echo cnTemplatePart::walker( 'term-list', $atts );
		}

		/**
		 * Callback for the `cn_action_list_before` filter,
		 *
		 * Display the category list before the results list.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $atts The shortcode options array passed by the `cn_action_list_before` filter.
		 */
		public static function viewBefore( $atts = array() ) {

			if ( is_admin() ) {

				return;
			}

			// The `cnen_` prefix is used to designate the Enhanced Categories shortcode options, and so they do not
			// conflict with core shortcode options. This is to allow overriding of the options values saved in the settings.
			$defaults = array(
				'cnec_display_categories'        => 1 == cnSettingsAPI::get( 'connections_categories', 'general', 'display' ),
				'cnec_display_breadcrumb'        => 1 == cnSettingsAPI::get( 'connections_categories', 'general', 'breadcrumb' ),
				'force_home'                     => false,
				'home_id'                        => cnSettingsAPI::get( 'connections', 'connections_home_page', 'page_id' ),

				// Support core term list options.
				'show_option_none'               => '',
				// 'child_of'                     => 0,

				// Support related core shortcode options.
				'category'                       => array(),
				'exclude_category'               => '',

				// Support related template shortcode options.
				'enable_category_by_root_parent' => false,
			);

			$atts = cnSanitize::args( $atts, $defaults );

			if ( $atts['enable_category_by_root_parent'] ) {

				$atts['child_of'] = $atts['category'];
			}

			$atts['exclude'] = _parse::stringList( $atts['exclude_category'] );

			if ( $atts['cnec_display_breadcrumb'] ) {
// $atts['cache_bust'] = time();
				cnTemplatePart::categoryBreadcrumb(
					array(
						'link'       => true,
						'separator'  => cnSettingsAPI::get( 'connections_categories', 'entry', 'parent_separator' ),
						'force_home' => $atts['force_home'],
						'home_id'    => $atts['home_id'],
					)
				);
			}

			// Display only when enabled but not on the single entry profile/detail page.
			if ( $atts['cnec_display_categories'] && ! cnQuery::getVar( 'cn-entry-slug', false ) ) {

				// Whether or not to display the all categories or only the children categories of the current category being viewed.
				if ( 1 == cnSettingsAPI::get( 'connections_categories', 'general', 'display_children' ) ) {

					if ( $current = cnCategory::getCurrent() ) {

						$atts['child_of'] = $current->term_id;
					}
				}

				// Add the user ID to the $attr array, so it will be included when creating the hash for the cache fragment key.
				// The reason is that the category counts are dependent on the current user permissions.
				$user   = wp_get_current_user();
				$atts[] = $user->ID;

				$key   = hash( 'crc32b', json_encode( $atts ) );
				$group = 'cn_categories';

				$fragment = new cnFragment( $key, $group );

				if ( ! $fragment->get() ) {

					self::viewPage( $atts );

					$fragment->save();
				}
			}
		}

		/**
		 * Callback for the `cn_entry_output_category_item` filter.
		 *
		 * Make the categories names permalinks.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param string        $html
		 * @param cnTerm_Object $category
		 * @param int           $count
		 * @param int           $i
		 * @param array         $atts
		 * @param cnOutput      $entry
		 *
		 * @return string
		 */
		public static function categoryItem( $html, $category, $count, $i, $atts, $entry ) {

			global $wp_rewrite;

			if ( 1 == cnSettingsAPI::get( 'connections_categories', 'entry', 'permalink_enabled' ) ) {

				$rel = ( is_object( $wp_rewrite ) && $wp_rewrite->using_permalinks() ) ? 'rel="category tag"' : 'rel="category"';

				$url = cnTerm::permalink(
					$category,
					'category',
					array(
						'force_home' => $entry->directoryHome['force_home'],
						'home_id'    => $entry->directoryHome['page_id'],
					)
				);

				$permalink = '<a href="' . $url . '" ' . $rel . '>' . esc_html( $category->name ) . '</a>';

				$html = sprintf(
					'<%1$s class="cn-category-name cn-category" id="cn-category-%2$d">%3$s%4$s</%1$s>',
					$atts['item_tag'],
					$category->term_id,
					$permalink,
					$count > $i ? $atts['separator'] : ''
				);
			}

			return $html;
		}

		/**
		 * Callback for the `cn_term_list_class` filter.
		 *
		 * Add the class name to the <ul> based on the user chosen display style.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $class
		 * @param array $terms
		 * @param array $atts
		 *
		 * @return array
		 */
		public static function categoryListClass( $class, $terms, $atts ) {

			switch ( cnSettingsAPI::get( 'connections_categories', 'general', 'display_style' ) ) {

				case 'grid':
					$class[] = 'cn-category-image-container';
					break;

				default:
					$class[] = 'cn-category-list-container';
			}

			return $class;
		}

		/**
		 * Callback for the `cn_term_list_class` filter.
		 *
		 * Add the class name to the <ul> based on the user chosen display style.
		 *
		 * NOTE: This is applied only when viewing the "View All Categories" page.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $class
		 * @param array $terms
		 * @param array $atts
		 *
		 * @return array
		 */
		public static function categoryListClass_ViewAll( $class, $terms, $atts ) {

			$class[] = 'cn-category-list-container';

			return $class;
		}

		/**
		 * Callback for the `cn_term_list_item_class` filter.
		 *
		 * Add the class name to the <li> based on the user chosen display style.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array         $class
		 * @param cnTerm_Object $term
		 * @param int           $depth
		 * @param array         $args
		 *
		 * @return array
		 */
		public static function categoryListItemClass( $class, $term, $depth, $args ) {

			if ( 'grid' == cnSettingsAPI::get( 'connections_categories', 'general', 'display_style' ) ) {

				if ( 0 === $depth ) {
					$class[] = 'cn-category-image-block';
				}
			}

			return $class;
		}

		/**
		 * Callback for the `cn_term_list_item` filter.
		 *
		 * If the display style is the "Image Grid" , add the category image if it exists.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param string        $html
		 * @param cnTerm_Object $term
		 * @param int           $depth
		 * @param array         $args
		 *
		 * @return string
		 */
		public static function categoryListItem( $html, $term, $depth, $args ) {

			if ( 'grid' == cnSettingsAPI::get( 'connections_categories', 'general', 'display_style' ) ) {

				$count = $args['show_count'] ? '<span class="cn-cat-count">&nbsp;(' . esc_html( number_format_i18n( $term->count ) ) . ')</span>' : '';

				$url   = cnTerm::permalink( $term, 'category', $args );
				$name  = '<span class="cn-term-name">' . esc_html( $term->name ) . $count . '</span>';
				$image = self::getImageHTML( $term->term_id );

				return sprintf(
					'<a href="%1$s" title="%2$s">%3$s%4$s</a>',
					$url,
					esc_attr( $term->name ),
					$image,
					$name
				);

			}

			return $html;
		}

		/**
		 * Render the image/placeholder HTML.
		 *
		 * @link https://developer.wordpress.org/reference/functions/wp_get_attachment_image/
		 *
		 * @access private
		 * @since 1.0
		 *
		 * @param int $id Term ID.
		 *
		 * @return string
		 */
		public static function getImageHTML( $id ) {

			$size = 'cn-category-thumb';
			// $html = '';

			if ( $imageID = self::getImageID( $id ) ) {

				// Generate thumbnail of correct size.
				self::maybeResizeImage( $imageID );

				$image = wp_get_attachment_image( $imageID, $size );

				if ( 0 < strlen( $image ) ) {

					$html = '<span class="cn-term-image">' . $image . '</span>';

				} else {

					$html = self::getPlaceholderHTML( $size );

				}

			} else {

				$html = self::getPlaceholderHTML( $size );
			}

			return $html;
		}

		/**
		 * Generate a placeholder div when a category has no image attached.
		 *
		 * @access private
		 * @since 1.0
		 *
		 * @param string $size
		 *
		 * @return string
		 */
		private static function getPlaceholderHTML( $size ) {

			$html = '';

			$sizeMeta = self::getImageSize( $size );

			if ( false != $sizeMeta ) {

				$html = '<span class="cn-term-image-none"><div style="width: ' . esc_attr( $sizeMeta['width'] ) . 'px; height: ' . esc_attr( $sizeMeta['height'] ) . 'px;"></div></span>';
			}

			return apply_filters( 'cnec_placeholder_html', $html, $size, $sizeMeta );
		}

		/**
		 * Callback for the `cnec_placeholder_html` filter.
		 *
		 * Display a placeholder image from a lorem image service provider.
		 *
		 * Use for testing/dev purposes only, do not use for a live site. Respect the free service providers!
		 *
		 * @link http://loremflickr.com/
		 * @link http://lorempixel.com/
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param string $html
		 * @param string $size
		 * @param array  $sizeMeta
		 *
		 * @return string
		 */
		public static function loremImage( $html, $size, $sizeMeta ) {

			static $endpoints = array(
				'http://loremflickr.com',
				'http://lorempixel.com/',
			);

			$endpoint = current( $endpoints );

			if ( false === next( $endpoints ) ) reset( $endpoints );

			static $categories = array(
				'abstract',
				'animals',
				'business',
				'cats',
				'city',
				'food',
				'nightlife',
				'fashion',
				'people',
				'nature',
				'sports',
				'technics',
				'transport',
			);

			$name = current( $categories );

			if ( false === next( $categories ) ) {
				reset( $categories );
			}

			$src   = "{$endpoint}/{$sizeMeta['width']}/{$sizeMeta['height']}/$name/";
			$image = '<img width="' . esc_attr( $sizeMeta['width'] ) . '" height="' . $sizeMeta['height'] . '" src="' . esc_url( $src ) . '" />';

			return '<span class="cn-term-image-none">' . $image . '</span>';
		}

		/**
		 * Get the category image ID by term ID.
		 *
		 * @access public
		 * @since 1.0
		 *
		 * @param int $id
		 *
		 * @return int|false
		 */
		public static function getImageID( $id ) {

			$image = cnMeta::get( 'term', $id, 'image', true );

			return $image ? $image : false;
		}

		/**
		 * Get image meta data attached to category.
		 *
		 * @since 1.0
		 *
		 * @param int    $id
		 * @param string $size
		 *
		 * @return array|false
		 */
		public static function getImageData( $id, $size = 'thumbnail' ) {

			return wp_get_attachment_image_src( $id, $size );
		}

		/**
		 * Get the image URL from the image metadata.
		 *
		 * @since 1.0
		 *
		 * @param array $data
		 *
		 * @return false|string
		 */
		public static function getImageURL( $data ) {

			$url = $data[0];

			return $url ? $url : false;
		}

		/**
		 * If an image size for the image attached to a category does not have the correct size, create it
		 * and update attachment metadata.
		 *
		 * @since 1.0
		 *
		 * @param int $id Attachment ID.
		 */
		public static function maybeResizeImage( $id ) {

			if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {

				require_once ABSPATH . 'wp-admin/includes/image.php';
			}

			if ( $meta = wp_get_attachment_metadata( $id, true ) ) {

				if ( ! isset( $meta['sizes']['cn-category-thumb'] )
					 || ( isset( $meta['sizes']['cn-category-thumb'] ) && ( 250 != $meta['sizes']['cn-category-thumb']['width'] || 250 != $meta['sizes']['cn-category-thumb']['height'] ) )
				) {

					$filename    = get_attached_file( $id, true );
					$attach_data = wp_generate_attachment_metadata( $id, $filename );

					wp_update_attachment_metadata( $id, $attach_data );
				}
			}
		}

		/**
		 * Callback for the `cn_output_default_atts_category` filter.
		 *
		 * @internal
		 * @since 1.0
		 *
		 * @param array $atts
		 *
		 * @return array
		 */
		public static function categoryDefaultAtts( $atts ) {

			// $atts['label']            = cnSettingsAPI::get( 'connections_categories', 'entry', 'label' );
			$atts['type']             = cnSettingsAPI::get( 'connections_categories', 'entry', 'display_style' );
			$atts['separator']        = cnSettingsAPI::get( 'connections_categories', 'entry', 'separator' );
			$atts['parent_separator'] = cnSettingsAPI::get( 'connections_categories', 'entry', 'parent_separator' );

			if ( 1 == cnSettingsAPI::get( 'connections_categories', 'entry', 'permalink_enabled' ) ) {

				$atts['link'] = true;
			}

			if ( 1 == cnSettingsAPI::get( 'connections_categories', 'entry', 'display_hierarchy' ) ) {

				$atts['parents'] = true;
			}

			return $atts;
		}

		/**
		 * Callback for the `cn_output_atts_category` filter.
		 *
		 * Need to override the method attributes to ensure the separator can be set via the settings
		 * because many template set this value. This will override the setting value when using the
		 * function but this seems like it'll be a necessary evil for now.
		 *
		 * @param array $atts
		 *
		 * @return array
		 */
		public static function categoryAtts( $atts ) {

			$atts['separator'] = cnSettingsAPI::get( 'connections_categories', 'entry', 'separator' );

			return $atts;
		}

		/**
		 * Callback for the `cn_clean_entry_cache` and `cn_clean_term_cache` actions.
		 *
		 * Purge fragment caches after adding/editing and entry/category.
		 *
		 * @internal
		 * @since 1.0
		 */
		public static function clearFragments() {

			cnCache::clear( true, 'transient', 'cn_categories' );
		}
	}

	/**
	 * Start up the extension.
	 *
	 * @access public
	 * @since 1.0
	 *
	 * @return false|Connections_Categories
	 */
	function Connections_Categories() {

		if ( class_exists( 'connectionsLoad' ) ) {

			return Connections_Categories::instance();

		} else {

			add_action(
				'admin_notices',
				static function() {
					echo '<div id="message" class="error"><p><strong>ERROR:</strong> Connections must be installed and active in order use Connections Enhanced Categories.</p></div>';
				}
			);

			return false;
		}
	}

	add_action( 'Connections_Directory/Loaded', 'Connections_Categories' );
}
