<?php

/**
 * WPV_Cache
 *
 * Caching class for Views.
 *
 * This class is useful on a series of scenarios:
 * 	- Parametric search with dependencies or counters, including post relationship filters.
 * 	- Invalidating stored cache in transients for Views output and meta keys
 *
 * @since 1.12
 */

class WPV_Cache {

	static $stored_cache									= array();
	static $stored_cache_extended_for_post_relationship     = array();
	static $stored_relationship_cache						= array();

	static $invalidate_views_cache_flag						= false;

	static $collected_parametric_search_filter_attributes	= array();

	function __construct() {

		/**
		 * =============================================
         *  Cache invalidation
		 * =============================================
         */

        // Invalidation on post and postmeta changes
        add_action( 'transition_post_status',		array( $this, 'invalidate_views_cache' ) );
        add_action( 'save_post',					array( $this, 'invalidate_views_cache' ) );
        add_action( 'delete_post',					array( $this, 'invalidate_views_cache' ) );
        add_action( 'added_post_meta',				array( $this, 'invalidate_views_cache' ) );
        add_action( 'updated_post_meta',			array( $this, 'invalidate_views_cache' ) );
        add_action( 'deleted_post_meta',			array( $this, 'invalidate_views_cache' ) );
        // Invalidation on term changes
        add_action( 'create_term',					array( $this, 'invalidate_views_cache' ) );
        add_action( 'edit_terms',					array( $this, 'invalidate_views_cache' ) );
        add_action( 'delete_term',					array( $this, 'invalidate_views_cache' ) );
        // Invalidation on user and usermeta changes
        add_action( 'user_register',				array( $this, 'invalidate_views_cache' ) );
        add_action( 'profile_update',				array( $this, 'invalidate_views_cache' ) );
        add_action( 'delete_user',					array( $this, 'invalidate_views_cache' ) );
        add_action( 'added_user_meta',				array( $this, 'invalidate_views_cache' ) );
        add_action( 'updated_user_meta',			array( $this, 'invalidate_views_cache' ) );
        add_action( 'deleted_user_meta',			array( $this, 'invalidate_views_cache' ) );
        // Invalidation on Types-related events
        add_action( 'types_fields_group_saved',		array( $this, 'invalidate_views_cache' ) );
        // Invalidation on Views-related events
        add_action( 'wpv_action_wpv_save_item',		array( $this, 'invalidate_views_cache_action' ) );
        add_action( 'wpv_action_wpv_import_item',	array( $this, 'invalidate_views_cache' ) );

		// Invalidate the shortcodes GUI transient data
		add_action( 'save_post',					array( $this, 'delete_shortcodes_gui_transients_action' ), 10, 2 );
		add_action( 'delete_post',					array( $this, 'delete_shortcodes_gui_transients_action' ), 10 );
		add_action( 'wpv_action_wpv_save_item',		array( $this, 'delete_shortcodes_gui_transients_action' ) );
		// Custom action
		add_action( 'wpv_action_wpv_delete_transient_shortcodes_gui_views',		array( $this, 'delete_shortcodes_gui_views_transient_action' ) );
		add_action( 'wpv_action_wpv_delete_transient_shortcodes_gui_cts',		array( $this, 'delete_shortcodes_gui_cts_transient_action' ) );

		// Execution!!!
		add_action( 'shutdown',						array( $this, 'maybe_clear_cache' ) );
	}

	/**
	 * Restart the stored cache.
	 *
	 * @since unknown
	 */
	static function restart_cache() {
		self::$stored_cache = array();
	}

	/**
	 * Process the filter_meta_html content, find the wpv-control and wpv-control-set shortcodes and extract their attributes.
	 * Transform that data into something that WPV_Cache can use.
	 *
	 * @param array $view_settings     The object settings
	 * @param array $override_settings Additional settings that will override the ones in $view_settings and needed to perform this action:
	 * 		'post_type' array The post types that the current object will be returning. Needed as WordPress Archives get this on-the-fly.
	 *
	 * @since 2.1.0
	 * @since 2.4.0 Add flags to cache post author data and post type data.
	 * @since 2.4.0 Abstract out the fake shortcodes definitions.
	 * @since m2m Add flag to cache post relationship data in the m2m format.
	 */
	static function get_parametric_search_data_to_cache( $view_settings = array(), $override_settings = array() ) {
		$parametric_search_data_to_cache = array(
			'cf'			=> array(),
			'tax'			=> array(),
			'post_author'	=> 'disabled',
			'post_type'		=> 'disabled'
		);
		if (
			! isset( $view_settings['filter_meta_html'] )
			|| strpos( $view_settings['filter_meta_html'], '[wpv-control' ) === false
		) {
			return $parametric_search_data_to_cache;
		}

		foreach ( $override_settings as $override_key => $override_value ) {
			$view_settings[ $override_key ] = $override_value;
		}

		global $shortcode_tags;
		self::$collected_parametric_search_filter_attributes = array();
		// Back up current registered shortcodes and clear them all out
		$orig_shortcode_tags = $shortcode_tags;
		remove_all_shortcodes();

		WPV_Cache::fake_control_shortcodes_callback();

		do_shortcode( $view_settings['filter_meta_html'] );
		$shortcode_tags = $orig_shortcode_tags;

		if ( strpos( $view_settings['filter_meta_html'], '[wpv-control-post-author' ) !== false ) {
			$parametric_search_data_to_cache['post_author'] = 'enabled';
		}

		if ( strpos( $view_settings['filter_meta_html'], '[wpv-control-post-type' ) !== false ) {
			$parametric_search_data_to_cache['post_type'] = 'enabled';
		}

		foreach ( self::$collected_parametric_search_filter_attributes as $atts_set ) {
			if ( isset( $atts_set['ancestors'] ) ) {
				$types_condition = new Toolset_Condition_Plugin_Types_Active();
				if ( $types_condition->is_met() ) {
					if ( apply_filters( 'toolset_is_m2m_enabled', false ) ) {
						$filters_manager = WPV_Filter_Manager::get_instance();
						$filter = $filters_manager->get_filter( Toolset_Element_Domain::POSTS, 'relationship' );
						$relationship_tree = $filter->get_relationship_tree( $atts_set['ancestors'] );
						$parametric_search_data_to_cache['relationship'] = $relationship_tree;
					} else {
						$filter_manager = WPV_Filter_Manager::get_instance();
						$post_relationship_filter = $filter_manager->get_filter( Toolset_Element_Domain::POSTS, 'relationship' );
						$returned_post_types = $post_relationship_filter->get_returned_post_types( $view_settings );
						$returned_post_type_parents = array();
						if ( empty( $returned_post_types ) ) {
							$returned_post_types = array( 'any' );
						}
						foreach ( $returned_post_types as $returned_post_type_slug ) {
							$parent_parents_array = wpcf_pr_get_belongs( $returned_post_type_slug );
							if ( $parent_parents_array != false && is_array( $parent_parents_array ) ) {
								$returned_post_type_parents = array_merge( $returned_post_type_parents, array_values( array_keys( $parent_parents_array ) ) );
							}
						}
						foreach ( $returned_post_type_parents as $parent_to_cache ) {
							$parametric_search_data_to_cache['cf'][] = '_wpcf_belongs_' . $parent_to_cache . '_id';
						}
					}
				}
			} else if ( isset( $atts_set['taxonomy'] ) ) {
				$parametric_search_data_to_cache['tax'][] = $atts_set['taxonomy'];
			} else if ( isset( $atts_set['auto_fill'] ) ) {
				$parametric_search_data_to_cache['cf'][] = _wpv_get_field_real_slug( $atts_set['auto_fill'] );
			} else if ( isset( $atts_set['field'] ) ) {
				$parametric_search_data_to_cache['cf'][] = _wpv_get_field_real_slug( $atts_set['field'] );
			}
		}
		self::$collected_parametric_search_filter_attributes = array();
		return $parametric_search_data_to_cache;
	}

	/**
	 * Register dummy callbacks for all the control shortcodes, to collect their attributes.
	 *
	 * This method will populate the self::collected_parametric_search_filter_attributes property
	 * with the right shortcode attributes sets.
	 *
	 * @since 2.4.0
	 */
	static function fake_control_shortcodes_callback() {

		add_shortcode( 'wpv-control-post-taxonomy',		array( 'WPV_Cache', 'collect_shortcode_attributes' ) );
		add_shortcode( 'wpv-control-postmeta',			array( 'WPV_Cache', 'collect_shortcode_attributes' ) );
		add_shortcode( 'wpv-control-post-relationship',	array( 'WPV_Cache', 'collect_shortcode_attributes' ) );
		add_shortcode( 'wpv-control',		array( 'WPV_Cache', 'collect_shortcode_attributes' ) );
		add_shortcode( 'wpv-control-set',	array( 'WPV_Cache', 'collect_shortcode_attributes' ) );

	}

	/**
	 * Dummy helper callback for collecting shortcode attributes.
	 *
	 * @param array  $atts
	 * @param string $content
	 *
	 * @since 2.1.0
	 */
	static function collect_shortcode_attributes( $atts, $content = null ) {
		self::$collected_parametric_search_filter_attributes[] = $atts;
		return;
	}

	/**
	 * Generate the potmeta cache for a set of posts and a seleted list of field keys.
	 *
	 * @param array $cache_post_meta The already existing postmeta cache
	 * @param array $id_posts        The lists of posts to generate the cache for
	 * @param array $fields_to_cache The list of fields to generate the cache for
	 *
	 * @return array
	 *
	 * @since 2.4.0
	 */
	static function generate_postmeta_cache( $cache_post_meta = array(), $id_posts = array(), $fields_to_cache = array() ) {
		// Sanitize $id_posts
		// It usually comes from a WP_Query, but still
		$id_posts = array_map( 'esc_attr', $id_posts );
		$id_posts = array_map( 'trim', $id_posts );
		// is_numeric does sanitization
		$id_posts = array_filter( $id_posts, 'is_numeric' );
		$id_posts = array_map( 'intval', $id_posts );

		if ( is_array( $cache_post_meta ) ) {
			$exclude_ids = array_keys( $cache_post_meta );
			$id_posts = array_diff( $id_posts, $exclude_ids );
		} else {
			$cache_post_meta = array();
		}

		$id_posts_postmeta_matched = array();
		$id_posts_postmeta_missed = array();

		if (
			! empty( $fields_to_cache )
			&& ! empty( $id_posts )
		) {
			global $wpdb;
			$id_list = implode( ',', $id_posts );
			$fields_to_cache_count = count( $fields_to_cache );
			$fields_to_cache_placeholders = array_fill( 0, $fields_to_cache_count, '%s' );
			$meta_list = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT post_id, meta_key, meta_value FROM {$wpdb->postmeta}
					WHERE post_id IN ({$id_list})
					AND meta_key IN (" . implode( ",", $fields_to_cache_placeholders ) . ")
					ORDER BY post_id ASC",
					$fields_to_cache
				),
				ARRAY_A
			);
			if ( ! empty( $meta_list ) ) {
				foreach ( $meta_list as $metarow ) {
					$mpid = intval( $metarow['post_id'] );
					$mkey = $metarow['meta_key'];
					$mval = $metarow['meta_value'];
					if ( ! in_array( $mpid, $id_posts_postmeta_matched ) ) {
						$id_posts_postmeta_matched[] = $mpid;
					}
					if (
						isset( $cache_post_meta[ $mpid ] )
					) {
						// The post has already been cached, let's check whether its meta key has been cached too
						if ( ! isset( $cache_post_meta[ $mpid ][ $mkey ] ) ) {
							$cache_post_meta[ $mpid ][ $mkey ] = array();
							$cache_post_meta[ $mpid ][ $mkey ][] = $mval;
						}
					} else {
						// We add to $cache_post_meta
						$cache_post_meta[ $mpid ] = array();
						$cache_post_meta[ $mpid ][ $mkey ] = array();
						$cache_post_meta[ $mpid ][ $mkey ][] = $mval;
					}
				}
			}
			// Fill the gaps
			$id_posts_postmeta_missed = array_diff( $id_posts, $id_posts_postmeta_matched );
			foreach ( $id_posts_postmeta_missed as $id_needed ) {
				if ( ! isset( $cache_post_meta[ $id_needed ] ) ) {
					$cache_post_meta[ $id_needed ] = array();
				}
			}
		}
		return $cache_post_meta;
	}

	/**
	 * Generate the taxonomy cache for a set of posts and a seleted list of taxonomies.
	 *
	 * @param array $cache_post_taxes The already existing taxonomy cache
	 * @param array $id_posts         The lists of posts to generate the cache for
	 * @param array $tax_to_cache     The list of taxonomies to generate the cache for
	 *
	 * @return array
	 *
	 * @note The settings might be polluted by non-existing taxonomies, so we need to intersect it.
	 *
	 * @since 2.4.0
	 */
	static function generate_taxonomy_cache( $cache_post_taxes = array(), $id_posts = array(), $tax_to_cache = array() ) {
		// Sanitize $id_posts
		// It usually comes from a WP_Query, but still
		$id_posts = array_map( 'esc_attr', $id_posts );
		$id_posts = array_map( 'trim', $id_posts );
		// is_numeric does sanitization
		$id_posts = array_filter( $id_posts, 'is_numeric' );
		$id_posts = array_map( 'intval', $id_posts );

		$current_taxonomies = get_taxonomies( '', 'names' );
		$tax_to_cache = array_intersect( $tax_to_cache, $current_taxonomies );
		$tax_to_cache = array_values( $tax_to_cache );
		if (
			! empty( $tax_to_cache )
			&& ! empty( $id_posts )
		) {
			$terms = wp_get_object_terms( $id_posts, $tax_to_cache, array( 'fields' => 'all_with_object_id' ) );
			if ( is_wp_error( $terms ) ) {
				$terms = array();
			}
			$object_terms = array();
			foreach ( (array) $terms as $term ) {
				$object_terms[ $term->object_id ][ $term->taxonomy ][ $term->term_id ] = $term;
			}
			foreach ( $id_posts as $id_needed ) {
				foreach ( $tax_to_cache as $taxonomy ) {
					if ( ! isset( $object_terms[ $id_needed ][ $taxonomy ] ) ) {
						if ( ! isset( $object_terms[ $id_needed ] ) ) {
							$object_terms[ $id_needed ] = array();
						}
						$object_terms[ $id_needed ][ $taxonomy ] = array();
					}
				}
			}
			foreach ( $object_terms as $post_id => $value ) {
				foreach ( $value as $taxonomy => $terms ) {
					if ( ! isset( $cache_post_taxes[ $taxonomy . '_relationships' ] ) ) {
						$cache_post_taxes[ $taxonomy . '_relationships' ] = array();
					}
					if ( ! isset( $cache_post_taxes[ $taxonomy . '_relationships' ][ $post_id ] ) ) {
						$cache_post_taxes[ $taxonomy . '_relationships' ][ $post_id ] = $terms;
					}
				}
			}
		}
		return $cache_post_taxes;
	}

	/**
	 * Generate the post data cache for a set of posts and a seleted list of post columns.
	 *
	 * @param array $cache_post_data    The already existing post data cache, each key matches a post column
	 * @param array $id_posts           The lists of posts to generate the cache for
	 * @param array $post_data_to_cache The list of post data columns to generate the cache for
	 *
	 * @return array
	 *
	 * @since 2.4.0
	 */
	static function generate_post_data_cache( $cache_post_data = array(), $id_posts = array(), $post_data_to_cache = array() ) {
		// Sanitize $id_posts
		// It usually comes from a WP_Query, but still
		$id_posts = array_map( 'esc_attr', $id_posts );
		$id_posts = array_map( 'trim', $id_posts );
		// is_numeric does sanitization
		$id_posts = array_filter( $id_posts, 'is_numeric' );
		$id_posts = array_map( 'intval', $id_posts );

		$columns_to_cache = array();
		$id_posts_for_column = array();
		$fields_to_query = array( 'ID' );

		if ( count( $post_data_to_cache ) > 0 ) {
			foreach ( $post_data_to_cache as $post_data_column => $post_data_status ) {
				$id_posts_for_column[ $post_data_column ] = $id_posts;
				if ( 'enabled' == $post_data_status ) {
					$columns_to_cache[] = $post_data_column;
					$fields_to_query[] = $post_data_column;
					if (
						isset( $cache_post_data[ $post_data_column ] )
						&& is_array( $cache_post_data[ $post_data_column ] )
						&& ! empty( $cache_post_data[ $post_data_column ] )
					) {
						$exclude_ids_for_column = call_user_func_array( 'array_merge', $cache_post_data[ $post_data_column ] );
						$id_posts_for_column[ $post_data_column ] = array_diff( $id_posts, $exclude_ids_for_column );
					} else {
						$cache_post_data[ $post_data_column ] = array();
					}
				}
			}
			$id_posts = call_user_func_array( 'array_merge', $id_posts_for_column );
		}
		$id_posts = array_values( $id_posts );

		if (
			! empty( $id_posts )
			&& count( $columns_to_cache ) > 0
		) {
			global $wpdb;
			$id_list = implode( ',', $id_posts );
			$post_data_list = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT %s FROM {$wpdb->posts}
					WHERE ID IN ({$id_list})
					AND 1 = %d
					ORDER BY ID ASC",
					implode( ',', $fields_to_query ),
					1
				),
				ARRAY_A
			);
			if ( ! empty( $post_data_list ) ) {
				foreach ( $post_data_list as $post_data_list_row ) {
					foreach ( $columns_to_cache as $column_key ) {
						if ( isset( $cache_post_data[ $column_key ][ $post_data_list_row[ $column_key ] ] ) ) {
							if ( ! in_array( $post_data_list_row['ID'], $cache_post_data[ $column_key ][ $post_data_list_row[ $column_key ] ] ) ) {
								$cache_post_data[ $column_key ][ $post_data_list_row[ $column_key ] ][] = $post_data_list_row['ID'];
							}
						} else {
							$cache_post_data[ $column_key ][ $post_data_list_row[ $column_key ] ] = array( $post_data_list_row['ID'] );
						}
					}
				}
			}
		}
		return $cache_post_data;
	}

	/**
	 * Generate the needed cache for the post relationship filter.
	 *
	 * The generated cache follows this structure:
	 * [relationship_slug] => array(
	 *     [ancestor_id] => array(
	 *         post_id_1,
	 *         post_id_2
	 *         ...
	 *     )
	 * )
	 *
	 * @param array $cache_post_relationship
	 * @param array $id_posts Post IDs to generate the cache for
	 * @param array $relationship_to_cache
	 *
	 * @return array
	 *
	 * @since m2m
	 */
	static function generate_post_relationship_cache( $cache_post_relationship = array(), $id_posts = array(), $relationship_to_cache = array() ) {
		// Sanitize $id_posts
		// It usually comes from a WP_Query, but still
		$id_posts = array_map( 'esc_attr', $id_posts );
		$id_posts = array_map( 'trim', $id_posts );
		// is_numeric does sanitization
		$id_posts = array_filter( $id_posts, 'is_numeric' );
		$id_posts = array_map( 'intval', $id_posts );

		if (
			! apply_filters( 'toolset_is_m2m_enabled', false )
			|| empty( $relationship_to_cache )
			|| empty( $id_posts )
		) {
			return $cache_post_relationship;
		}

		do_action( 'toolset_do_m2m_full_init' );

		$direct_relationship = end( $relationship_to_cache );

		$association_query = new Toolset_Association_Query_V2();
		if ( ! empty( $direct_relationship['relationship'] ) ) {
			$association_query->add( $association_query->relationship_slug( $direct_relationship['relationship'] ) );
		}
		$association_query->limit( PHP_INT_MAX )
			->add( $association_query->multiple_elements(
				$id_posts, Toolset_Element_Domain::POSTS, Toolset_Relationship_Role::role_from_name( $direct_relationship['role_target'] )
			) );

		$associations = $association_query->get_results();

		if ( empty( $associations ) ) {
			return $cache_post_relationship;
		}

		foreach ( $associations as $association ) {
			$association_current_role = Toolset_Relationship_Role::role_from_name( $direct_relationship['role_target'] );
			$current_id = $association->get_element_id( $association_current_role );
			$association_ancestor_role = Toolset_Relationship_Role::role_from_name( $direct_relationship['role'] );
			$ancestor_id = $association->get_element_id( $association_ancestor_role );

			$cache_post_relationship[ $direct_relationship['type'] ] =
				isset( $cache_post_relationship[ $direct_relationship['type'] ] )
				? $cache_post_relationship[ $direct_relationship['type'] ]
				: array();

			$cache_post_relationship[ $direct_relationship['type'] ][ $ancestor_id ] =
				isset( $cache_post_relationship[ $direct_relationship['type'] ][ $ancestor_id ] )
				? $cache_post_relationship[ $direct_relationship['type'] ][ $ancestor_id ]
				: array();

			$cache_post_relationship[ $direct_relationship['type'] ][ $ancestor_id ][] = $current_id;
			$cache_post_relationship[ $direct_relationship['type'] ][ $ancestor_id ] = array_unique( $cache_post_relationship[ $direct_relationship['type'] ][ $ancestor_id ] );
		}

		array_pop( $relationship_to_cache );
		if ( null === $relationship_to_cache ) {
			return $cache_post_relationship;
		}

		foreach( $cache_post_relationship as $associated_relationship => $associated_data ) {
			$associated_data_ids = array_keys( $associated_data );
			$cache_post_relationship = self::generate_post_relationship_cache( $cache_post_relationship, $associated_data_ids, $relationship_to_cache );
		}

		return $cache_post_relationship;
	}

	/**
	 * Mimic the caching construction of WordPress so we can use it for counting posts.
	 * update_postmeta_cache should get cached data, so we avoind further queries for postmeta.
	 * We still need to generate the cache for the given taxonomies and post data.
	 *
	 * @param array $id_posts List of post IDs
	 * @param array $to_cache List of data to pseudo-cache
	 *    'tax'         array  List of taxonomy names to cache
	 *    'post_author' string Whether to cache the author of posts
	 *    'post_type'   string Whether to cache the type of posts
	 *
	 * @return array Cached data compatible with the native $wp_object_cache->cache format
	 *
	 * @uses update_postmeta_cache
	 *
	 * @since 1.12.0
	 * @since 2.4.0 Add flags to cache post author data and post type data.
	 * @since 2.4.0 Abstract out the taxonomy and post data cache generator.
	 */
	static function generate_native_cache( $id_posts = array(), $to_cache = array() ) {
		$tax_to_cache		= ( isset( $to_cache['tax'] ) ) ? $to_cache['tax'] : array();
		$post_data_to_cache	= array(
			'post_author'	=> ( isset( $to_cache['post_author'] ) ) ? $to_cache['post_author'] : 'disabled',
			'post_type'		=> ( isset( $to_cache['post_type'] ) ) ? $to_cache['post_type'] : 'disabled'
		);
		$relationship_to_cache = ( isset( $to_cache['relationship'] ) ) ? $to_cache['relationship'] : array();

		// Sanitize $id_posts
		// It usually comes from a WP_Query, but still
		$id_posts = array_map( 'esc_attr', $id_posts );
		$id_posts = array_map( 'trim', $id_posts );
		// is_numeric does sanitization
		$id_posts = array_filter( $id_posts, 'is_numeric' );
		$id_posts = array_map( 'intval', $id_posts );

		// First, the postmeta cache
		$cache_post_meta = update_postmeta_cache( $id_posts );

		// Then, the taxonomies
		$cache_post_taxes = array();
		$cache_post_taxes = WPV_Cache::generate_taxonomy_cache( $cache_post_taxes, $id_posts, $tax_to_cache );

		// Finally, the post data
		$cache_post_data = array(
			'post_author'	=> array(),
			'post_type'		=> array()
		);
		$cache_post_data = WPV_Cache::generate_post_data_cache( $cache_post_data, $id_posts, $post_data_to_cache );

		$cache_combined = array();
		$cache_combined['post_meta'] = $cache_post_meta;
		foreach ( $cache_post_taxes as $tax_key => $tax_cached_values ) {
			$cache_combined[ $tax_key ] = $tax_cached_values;
		}
		$cache_combined['post_author'] = $cache_post_data['post_author'];
		$cache_combined['post_type'] = $cache_post_data['post_type'];

		$cache_post_relationship = array();
		$cache_combined['post_relationship'] = WPV_Cache::generate_post_relationship_cache( $cache_post_relationship, $id_posts, $relationship_to_cache );

		self::$stored_cache = $cache_combined;

		return $cache_combined;
	}



	/**
	 * Mimic the caching construction of WordPress so we can use it for counting posts.
	 * Caches data for the passed custom fields, taxonomies and post data, without adding it to self::$stored_cache
	 *
	 * @param array $id_posts List of post IDs
	 * @param array $to_cache List of data to pseudo-cache
	 *    'tax'         array  Lost of taxonomy names to cache
	 *    'cf'          array  List of of field meta_key's to cache
	 *    'post_author' string Whether to cache the author of posts
	 *    'post_type'   string Whether to cache the type of posts
	 *
	 * @return array Cached data compatible with the native $wp_object_cache->cache format
	 *
	 * @since 1.12.0
	 * @since 2.4.0 Add flags to cache post author data and post type data.
	 * @since 2.4.0 Abstract out the taxonomy and post data cache generator.
	 */
	static function generate_auxiliar_cache( $id_posts = array(), $to_cache = array() ) {
		$cache_combined		= self::$stored_cache;
		$fields_to_cache	= ( isset( $to_cache['cf'] ) ) ? $to_cache['cf'] : array();
		$tax_to_cache		= ( isset( $to_cache['tax'] ) ) ? $to_cache['tax'] : array();
		$post_data_to_cache	= array(
			'post_author'	=> ( isset( $to_cache['post_author'] ) ) ? $to_cache['post_author'] : 'disabled',
			'post_type'		=> ( isset( $to_cache['post_type'] ) ) ? $to_cache['post_type'] : 'disabled'
		);
		$relationship_to_cache = ( isset( $to_cache['relationship'] ) ) ? $to_cache['relationship'] : array();

		// Sanitize $id_posts
		// It usually comes from a WP_Query, but still
		$id_posts = array_map( 'esc_attr', $id_posts );
		$id_posts = array_map( 'trim', $id_posts );
		// is_numeric does sanitization
		$id_posts = array_filter( $id_posts, 'is_numeric' );
		$id_posts = array_map( 'intval', $id_posts );

		// First, the postmeta cache
		$cache_post_meta = isset( $cache_combined['post_meta'] ) ? $cache_combined['post_meta'] : array();
		$cache_combined['post_meta'] = WPV_Cache::generate_postmeta_cache( $cache_post_meta, $id_posts, $fields_to_cache );

		// Then, the taxonomies
		$cache_combined = WPV_Cache::generate_taxonomy_cache( $cache_combined, $id_posts, $tax_to_cache );

		// Finally, the post data
		$cache_post_data = array(
			'post_author'	=> ( isset( $cache_combined['post_author'] ) ) ? $cache_combined['post_author'] : array(),
			'post_type'		=> ( isset( $cache_combined['post_type'] ) ) ? $cache_combined['post_type'] : array(),
		);
		$cache_post_data = WPV_Cache::generate_post_data_cache( $cache_post_data, $id_posts, $post_data_to_cache );

		$cache_combined['post_author'] = $cache_post_data['post_author'];
		$cache_combined['post_type'] = $cache_post_data['post_type'];

		$cache_post_relationship = isset( $cache_combined['post_relationship'] ) ? $cache_combined['post_relationship'] : array();
		$cache_combined['post_relationship'] = WPV_Cache::generate_post_relationship_cache( $cache_post_relationship, $id_posts, $relationship_to_cache );

		return $cache_combined;
	}

	/**
	 * Mimics the caching construction of WordPress so we can use it for counting posts.
	 * Caches data for the passed custom fields, taxonomies and post data, and adds it to self::$stored_cache
	 *
	 * @param array $id_posts List of post IDs
	 * @param array $to_cache List of data to pseudo-cache
	 *    'tax'         array  List of taxonomy names to cache
	 *    'cf'          array  List of field meta_key's to cache
	 *    'post_author' string Whether to cache the author of posts
	 *    'post_type'   string Whether to cache the type of posts
	 *    'relationship' string Whether to generate the relationship cache
	 *
	 * @uses self::generate_auxiliar_cache
	 *
	 * @return (array) cached data compatible with the native $wp_object_cache->cache format
	 *
	 * @since 1.12
	 */
	static function generate_cache( $id_posts = array(), $to_cache = array() ) {
		$cache_combined = self::generate_auxiliar_cache( $id_posts, $to_cache );
		self::$stored_cache = $cache_combined;
		return $cache_combined;
	}

	/**
	 * Mimic the caching construction of WordPress so we can use it for counting posts.
	 * Caches data for the passed custom fields and taxonomies, and adds it to self::$stored_cache_extended_for_post_relationship
	 * Used when rendering post relationship filters with dependency or counters,
	 * as we need to generate a specific query that avoinds the filter by the current post type in the relationship ree, if it exists
	 *
	 * @param array $id_posts List of post IDs
	 * @param array $to_cache List of data to pseudo-cache
	 *    'tax'         array  List of taxonomy names to cache
	 *    'cf'          array  List of field meta_key's to cache
	 *    'post_author' string Whether to cache the author of posts
	 *    'post_type'   string Whether to cache the type of posts
	 *    'relationship' string Whether to generate the relationship cache
	 *
	 * @uses self::generate_auxiliar_cache
	 *
	 * @return array Cached data compatible with the native $wp_object_cache->cache format
	 *
	 * @since 1.12
	 * @since m2m Keep for bakwards compatibility on sites that do not switch to M2M
	 */
	static function generate_cache_extended_for_post_relationship( $id_posts = array(), $to_cache = array() ) {
		$cache_combined = self::generate_auxiliar_cache( $id_posts, $to_cache );
		self::$stored_cache_extended_for_post_relationship = $cache_combined;
		return $cache_combined;
	}

	/**
	 * Generate data for counters and disable/hide elements in a post relationship parametric filter
	 *
	 * @param $tree_array array List of ancestors, in a top-to-bottom order
	 * @param $count bool Whether the count should return the number of matches or just a true/false statement
	 *
	 * @return array
	 *
	 * @since 1.12
	 * @note This is being generated per ancestor shortcode in a relationship query:
	 *       We should be able to cache it per relationship shortcode and do it once.
	 *       At least when no ancestor filter has been submitted yet...
	 */
	static function generate_post_relationship_tree_cache( $tree_array, $count = true ) {
		if ( apply_filters( 'toolset_is_m2m_enabled', false ) ) {
			return self::generate_post_relationship_tree_cache_from_m2m( $tree_array, $count );
		} else {
			return self::generate_post_relationship_tree_cache_from_postmeta( $tree_array, $count );
		}
	}

	private static function generate_post_relationship_tree_cache_from_postmeta( $tree_array, $count ) {
		$tree_real = array_reverse( $tree_array );
		$tree_remove = array_shift( $tree_real );
		$tree_ground = end( $tree_array );
		$tree_roof = reset( $tree_array );
		$current_post_ids = array();
		$counters = array();
		global $wpdb;
		$cache_combined = self::$stored_cache_extended_for_post_relationship;
		if (
			isset( $cache_combined['post_meta'] )
			&& is_array( $cache_combined['post_meta'] )
		) {
			$cached_postmeta = $cache_combined['post_meta'];
			$field = '_wpcf_belongs_' . $tree_ground . '_id';
			foreach ( $cached_postmeta as $key => $value ) {
				if ( isset( $value[ $field ] ) ) {
					$cached_postmeta[ $key ] = $value[ $field ];
				} else {
					unset( $cached_postmeta[ $key ] );
				}
			}
			$current_post_ids = array();
			if ( count( $cached_postmeta ) > 0 ) {
				$current_post_ids = call_user_func_array('array_merge', $cached_postmeta );
			}
			foreach ( $current_post_ids as $cpi ) {
				$meta_criteria_to_filter = array( '_wpcf_belongs_' . $tree_ground . '_id' => array( $cpi ) );
				$data = array();
				$data['list'] = $cache_combined['post_meta'];
				$data['args'] = $meta_criteria_to_filter;
				$data['kind'] = '';
				$data['comparator'] = 'equal';
				$data['count_matches'] = $count;
				$counters[ $cpi ] = array(
					'type'	=> $tree_ground,
					'count'	=> wpv_list_filter_checker( $data )
				);
			}
		}
		foreach ( $tree_real as $tree_branch ) {
			$current_post_ids = array_map( 'esc_attr', $current_post_ids );
			$current_post_ids = array_map( 'trim', $current_post_ids );
			$current_post_ids = array_filter( $current_post_ids, 'is_numeric' );
			$current_post_ids = array_map( 'intval', $current_post_ids );
			if ( count( $current_post_ids ) > 0 ) {
				$future_post_ids = $wpdb->get_results(
					$wpdb->prepare(
						"SELECT post_id, meta_value
						FROM {$wpdb->postmeta}
						WHERE meta_key = %s
						AND post_id IN ('" . implode( "','", $current_post_ids ) . "')",
						'_wpcf_belongs_' . $tree_branch . '_id'
					),
					ARRAY_A
				);
				foreach ( $future_post_ids as $fpid ) {
					$add = isset( $counters[ $fpid['post_id'] ] ) ? $counters[ $fpid['post_id'] ]['count'] : 1;
					if ( ! isset( $counters[ $fpid['meta_value'] ] ) ) {
						$counters[ $fpid['meta_value'] ] = array(
							'type'	=> $tree_branch,
							'count'	=> 0
						);
					}
					$counters[ $fpid['meta_value'] ]['count'] = isset( $counters[ $fpid['meta_value'] ] ) ? ( $counters[ $fpid['meta_value'] ]['count'] + $add ) : $add;
				}
				$current_post_ids = wp_list_pluck( $future_post_ids, 'meta_value' );
			} else {
				$current_post_ids = array();
			}
		}
		self::$stored_relationship_cache = $counters;
		return $counters;
	}

	private static function generate_post_relationship_tree_cache_from_m2m( $tree_array, $count ) {
		$counters = array();
		global $wpdb;
		$cache_combined = self::$stored_cache_extended_for_post_relationship;
		if (
			! isset( $cache_combined['post_relationship'] )
			|| ! is_array( $cache_combined['post_relationship'] )
		) {
			return $counters;
		}

		$cached_post_relationship = $cache_combined['post_relationship'];
		if ( 0 === count( $cached_post_relationship ) ) {
			// The cache combined is relationship slug key-based,
			// following the reverse of the tree array
			return $counters;
		}
		$current_relationship = reset( $cached_post_relationship );
		$current_relationship_type = key( $cached_post_relationship );
		foreach ( $cached_post_relationship as $post_type_slug => $relationship_associations ) {
			foreach ( $relationship_associations as $ancestor_id => $ancestor_children ) {
				$counters[ $ancestor_id ] = array(
					'type'	=> $post_type_slug,
					'count'	=> count( $ancestor_children )
				);
				if ( $current_relationship_type != $post_type_slug ) {
					$corrected_count = 0;
					foreach ( $ancestor_children as $ancestor_child_maybe_parent ) {
						if ( isset( $counters[ $ancestor_child_maybe_parent ] ) ) {
							$corrected_count += $counters[ $ancestor_child_maybe_parent ]['count'];
						}
					}
					$counters[ $ancestor_id ]['count'] = $corrected_count;
				}
			}
		}
		self::$stored_relationship_cache = $counters;
		return $counters;
	}

	/**
	* Merge the native and auxiliar caches for taxonomies
	*
	* Note that we can not use array_merge as it does not preserve indexes
	* We would get duplicates when the same page contains more than a View because the generate cache is run twice
	*
	* @param $tax_cache_combined	The existing cache
	* @param $tax_cached_values		The newly cached values
	*
	* @since 2.0
	*/

	static function merge_taxonomy_cache( $tax_cache_combined, $tax_cached_values ) {
		foreach ( $tax_cached_values as $tax_post_id => $tax_post_terms ) {
			if ( ! isset( $tax_cache_combined[ $tax_post_id ] ) ) {
				// If we already have cached data for this taxonomy and post, we should do nothing
				// Otherwise we add it
				$tax_cache_combined[ $tax_post_id ] = $tax_post_terms;
			}
		}
		return $tax_cache_combined;
	}

	/**
	* Invalidate Views first page cache if necessary - store flag
	*
	* @since 2.0
	*/

	function invalidate_views_cache( $p ) {
		self::$invalidate_views_cache_flag = true;
	}

	/**
	 * Invalidate wpv_transient_published_*** cache when:
	 * 	creating, updating or deleting a View
	 * 	creating, updating or deleting a Content Template
	 *
	 * @todo We might want to use a flag here, not sure
	 *
	 * @since 2.0.0
	 * @since 2.4.0 Clear the cache of the wpv_transient_pub_cts_for_cred_post and wpv_transient_pub_cts_for_cred_user transients
	 *     when creating, editing or removing a Content Template
	 */

	function delete_shortcodes_gui_transients_action( $post_id, $post = null  ) {
		if ( is_null( $post ) ) {
			$post = get_post( $post_id );
			if ( is_null( $post ) ) {
				return;
			}
		}
		$slugs = array( 'view', 'view-template' );
		if ( ! in_array( $post->post_type, $slugs ) ) {
			return;
		}
		switch ( $post->post_type ) {
			case 'view':
				delete_transient( 'wpv_transient_published_views' );
				break;
			case 'view-template':
				delete_transient( 'wpv_transient_published_cts' );
				delete_transient( 'wpv_transient_pub_cts_for_cred_post' );
				delete_transient( 'wpv_transient_pub_cts_for_cred_user' );
				break;

		}
	}

	/**
	* Invalidate wpv_transient_published_views cache manually
	*
	* @since 2.1
	*/

	function delete_shortcodes_gui_views_transient_action() {
		delete_transient( 'wpv_transient_published_views' );
	}

	/**
	* Invalidate wpv_transient_published_cts cache manually
	*
	* @since 2.1
	*/

	function delete_shortcodes_gui_cts_transient_action() {
		delete_transient( 'wpv_transient_published_cts' );
	}

	/**
	* Maybe delete cached data on shutdown
	*
	* @since 2.0
	*/

	public function maybe_clear_cache() {
		if ( self::$invalidate_views_cache_flag ) {
			$this->invalidate_views_cache_action();
		}
	}

	/**
	* Invalidate Views first page cache if necessary
	*
	* @since 2.0
	*/

    function invalidate_views_cache_action() {
        // Invalidate Views Cache when
        // - A (any post-type) Post is created/updated/trashed/deleted...
        // - A Taxonomy Term has been created/updated/...
        // - An User has been created/updated
        // - A View has been updated

        // Remove both [wpv-view] and [wpv-form-view] caches
        $cached_output_index = get_option( 'wpv_transient_view_index', array() );
		foreach( $cached_output_index as $cache_id => $v ) {
			$trasient = 'wpv_transient_view_'.$cache_id;
			delete_transient( $trasient );
		}
        delete_option( 'wpv_transient_view_index' );

        $cached_filter_index = get_option( 'wpv_transient_viewform_index', array() );
		foreach( $cached_filter_index as $cache_id => $v ) {
			$trasient = 'wpv_transient_viewform_'.$cache_id;
			delete_transient( $trasient );
		}
        delete_option( 'wpv_transient_viewform_index' );
    }

}

$wpv_cache_object = new WPV_Cache();
