<?php

namespace FilterEverything\Filter;

if ( ! defined('ABSPATH') ) {
    exit;
}

use FilterEverything\Filter\Pro\Entities\PostMetaExistsEntity;
use FilterEverything\Filter\Pro\Entities\TaxonomyNumEntity;

class EntityManager
{
    const DEFAULT_PREFIX = 'filter-';

    private $allConfiguredFilters;

    public function __construct()
    {
        $this->allConfiguredFilters = $this->getAllConfiguredFilters();
    }

    public static function getTaxonomies()
    {
        $excludedTaxes = flrt_excluded_taxonomies();
        $args = [];
        $taxonomies = get_taxonomies($args, 'objects');

        foreach ($taxonomies as $t => $taxonomy) {
            if (in_array($taxonomy->name, $excludedTaxes)) {
                unset($taxonomies[$t]);
            }
        }

        return $taxonomies;
    }

    private function getData( $key )
    {
        return Container::instance()->getParam( $key );
    }

    private function storeData( $key, $data )
    {
        Container::instance()->storeParam( $key, $data );
    }

    /**
     * @param $key = $entity.'-'.$name
    */
    public function createEntity( $key, $postType = '' )
    {
        if ( ! $key ) {
            return false;
        }

        $fse   = Container::instance()->getFilterService();
        $name  = '';
        $parts = explode( $fse->sep, $key, 2 );

        $entity = $parts[0];
        $name   = $parts[1];

        $storeKey = $key;
        /**
         * We add post_type suffix to the key for filters
         * which terms depend from post_type
         * */
        if ( in_array( $entity, array(
                'post_meta_num',
                'post_meta_exists',
                'post_meta',
                'tax_numeric',
                'author',
                'post_date',
                'post_meta_date',
                )
            ) && $postType ) {
            $storeKey = $key.'_'.$postType;
        }

        if ( $entityExists = $this->getData( $storeKey ) ) {
            return $entityExists;
        }

        switch( $entity ) {
            case 'taxonomy':
                $this->storeData( $storeKey, new TaxonomyEntity( $name ) );
            break;

            case 'post_meta':
                $this->storeData( $storeKey, new PostMetaEntity( $name, $postType ) );
            break;

            case 'post_meta_num':
                $this->storeData( $storeKey, new PostMetaNumEntity( $name, $postType ) );
            break;

            case 'post_meta_exists':
                if( class_exists('FilterEverything\Filter\Pro\Entities\PostMetaExistsEntity') ) {
                    $this->storeData($storeKey, new PostMetaExistsEntity( $name, $postType ) );
                }else{
                    // For the plugin version downgrade compatibility
                    $this->storeData($storeKey, new DefaultEntity( $name ) );
                }
                break;

            case 'author':
                $this->storeData( $storeKey, new AuthorEntity( $name, $postType ) );
            break;

            case 'tax_numeric':
                if ( class_exists('FilterEverything\Filter\Pro\Entities\TaxonomyNumEntity') ) {
                    $this->storeData( $storeKey, new TaxonomyNumEntity( $name, $postType ) );
                } else {
                    // For the plugin version downgrade compatibility
                    $this->storeData($storeKey, new DefaultEntity( $name ) );
                }

            break;

            case 'post_date':
                $this->storeData( $storeKey, new PostDateEntity( $name, $postType ) );
            break;

            case 'post_meta_date':
                $this->storeData( $storeKey, new PostMetaDateEntity( $name, $postType ) );
                break;
        }

        unset( $fse, $parts, $entity, $name );

        return $this->getData( $storeKey );
    }

    public function getEntityByFilter( $filter, $postType = '' )
    {
        $fse = Container::instance()->getFilterService();

        if ( ! isset( $filter['entity'] ) ) {
            return false;
        }

        if ( ! isset( $filter['e_name'] ) ) {
            return false;
        }

        return $this->createEntity( $fse->getEntityKey( $filter['entity'], $filter['e_name'] ), $postType );

    }

    public function getPossibleTaxonomies()
    {
        $entities   = [];
        $args       = apply_filters( 'wpc_get_taxonomies_args', [] );
        $taxonomies = get_taxonomies( $args, 'objects' );
        $excluded_taxonomies = flrt_excluded_taxonomies();

        foreach ( $taxonomies as $taxonomy ){
            if( in_array( $taxonomy->name, $excluded_taxonomies ) ){
                continue;
            }
            // It is better to save value as 'taxonomy_pa_size' because
            // user potentially can create post_meta with the same name
            $label = ucwords( flrt_ucfirst( mb_strtolower( $taxonomy->label ) ) );
            if( ! $label ){
                $label = $taxonomy->name;
            }
            $entities[ 'taxonomy_' . $taxonomy->name ] = $label;
        }

        return $entities;
    }

    public function getPossibleEntities()
    {
        $entities   = [];

        $entities['taxonomy']['group_label'] = esc_html__( 'Taxonomy Filters', 'filter-everything' );
        $entities['taxonomy']['entities'] = $this->getPossibleTaxonomies();

        $other = array(
            'post_meta' => array(
                'group_label' => esc_html__('Custom Field Filters', 'filter-everything'),
                'entities'    => array(
                    'post_meta'        => esc_html__('Custom Field', 'filter-everything'),
                    'post_meta_num'    => esc_html__('Custom Field Numeric', 'filter-everything'),
                    'post_meta_date'   => esc_html__('Custom Field Date', 'filter-everything'),
                    'post_meta_exists' => esc_html__('Custom Field Exists - Available in PRO', 'filter-everything'),
                )
            ),
            'other' => array(
                'group_label' => esc_html__( 'Other Filters', 'filter-everything' ),
                'entities' => array(
                    'post_date'     => esc_html__( 'Post Date', 'filter-everything' ),
                    'author_author' => esc_html__( 'Post Author', 'filter-everything' ),
                    'tax_numeric'   => esc_html__( 'Taxonomy Numeric - Available in PRO', 'filter-everything' ),
                    )
            )
        );

        $result = array_merge( $entities, $other );

        $result = apply_filters( 'wpc_possible_entities', $result );

        return $result;
    }

    public function prepareFilterCommon( $entityAndEname, $filters = array() )
    {
        $filter = [];
        $e_name = '';
        $fs = Container::instance()->getFilterService();

        $allPermalinksSettings  = get_option( 'wpc_filter_permalinks', [] );
        $entityName = explode( $fs->sep, $entityAndEname, 2 );

        $filter['e_name'] = $e_name = $fs->getEntityEname( $entityAndEname );
        $filter['entity'] = $entityName[0];

        if( isset( $allPermalinksSettings[$entityAndEname] ) && $allPermalinksSettings[$entityAndEname] ){
            $filter['slug'] = $allPermalinksSettings[$entityAndEname];
            // This is messed up because label is not good index for this item, but it already exists
            // And I will try ty change it later. Maybe. Very very maybe :-)
            $filter['label'] = '{'.$allPermalinksSettings[$entityAndEname].'}';
        }

        if( isset( $filters[$e_name] ) ){
            $filter['title'] = $filters[$e_name]['label'];
        }

        return $filter;
    }

    public function getCommonFilterValues( $eName, $postType )
    {
        $filter = [];

        if( ! $eName || ! $postType ){
            return false;
        }

        $fs = Container::instance()->getFilterService();
        $allIndexedFilters  = get_option( 'wpc_seo_rules_settings' );

        foreach ( $allIndexedFilters as $item => $value ) {
            $itemKey = explode(":", $item, 2 );
            $maybePostType = $itemKey[0]; // post|product etc

            if( $maybePostType === $postType ){
                $entityAndEname = $itemKey[1];
                $entityName = explode( $fs->sep, $entityAndEname, 2 );

                if( $entityName[1] === $eName ){
                    $filter = $this->prepareFilterCommon( $itemKey[1] );
                    break;
                }
            }
        }

        return $filter;
    }

    public function getFilterByEname( $e_name, $sets = [] )
    {
        $container = Container::instance();

        if( ! empty( $sets ) ){
            $actualSets = $sets;
        }else{
            $wpManager = $container->getWpManager();
            $actualSets = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
        }

        if( empty( $actualSets ) || ! $actualSets ){
            return false;
        }

        $theFilter = [];

        foreach ( $actualSets as $actualSet ) {
            $key = $e_name . '_'.$actualSet['ID'];
            if ( isset( $this->allConfiguredFilters[$key] ) ){
                $theFilter = $this->allConfiguredFilters[$key];
                break;
            }
        }

        return $theFilter;
    }

    public function getFilterById( $filter_id )
    {
        if( ! $filter_id ) {
            return [];
        }

        $theFilter = [];
        foreach( $this->allConfiguredFilters as $filter ){

            if( $filter['ID'] === $filter_id ){
                $theFilter = $filter;
                break;
            }
        }

        return $theFilter;
    }

    /**
     * @param string $slug filters prefix e.g. 'cat', 'tag'
     * @param array $onlyKeys optional. Keys from filter array that should be extracted
     * @return array all matched filters from DB.
     */
    public function getAllFiltersBySlug( $slug, $onlyKeys = [] )
    {
        $theFilters = [];

        foreach( $this->allConfiguredFilters as $filter ){
            if( $filter['slug'] === $slug ){
                $theFilters[] = $filter;
            }
        }

        if( ! empty( $onlyKeys ) ){
            $extractedFilters = [];
            foreach( $theFilters as $filter ){
                $extractedFilters[] = flrt_extract_vars( $filter, $onlyKeys );
            }
            return $extractedFilters;
        }

        return $theFilters;
    }

    public function getFilterBySlug( $slug )
    {
        $theFilter = [];

        foreach( $this->allConfiguredFilters as $filter ) {
            if ( $filter['slug'] === $slug ) {
                $theFilter = $filter;
                break;
            }
        }

//        if( ! empty( $onlyKeys ) ){
//            return flrt_extract_vars( $theFilter, $onlyKeys );
//        }

        return $theFilter;
    }

    public function getGlobalConfiguredSlugs()
    {
        $permalinksTab = new PermalinksTab();
        return get_option( $permalinksTab->optionName, [] );
    }

    public function getFlatEntities( $entities = false )
    {
        if( ! $entities ){
            $entities    = $this->getPossibleEntities();
        }

        $flat_entities = [];

        array_walk_recursive( $entities, function ( $value, $key ) use ( &$flat_entities ) {
            if( $key !== 'group_label' ){
                // $value = label
                $flat_entities[ $key ] = $value;
            }
        }, $flat_entities );

        return $flat_entities;
    }

    /**
     * @return array
    */
    public function extractBelongsFilters( $all_filters )
    {
        $result = [];
        if( ! is_array( $all_filters ) ){
            return $result;
        }

        $ffs = Container::instance()->getFilterFieldsService();

        foreach( $all_filters as $filter ) {
            if( $ffs->filterBelongsToPostType( $filter['parent'], $filter['entity'], $filter['e_name'] ) ){
                $result[ $filter['ID'] ] = $filter;
            }
        }

        return $result;
    }

    public function hasPostTypeFilters( $postType )
    {
        global $wpdb;

        $sql[] = "SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}";
        $sql[] = "WHERE {$wpdb->posts}.post_type = '%s'";
        $sql[] = "AND {$wpdb->posts}.post_status = 'publish'";
        $sql[] = "AND {$wpdb->posts}.post_excerpt = '%s'";
        $sql[] = "LIMIT 0, 1";

        $sql = implode(' ', $sql);

        $query      = $wpdb->prepare( $sql, FLRT_FILTERS_SET_POST_TYPE, $postType );
        $results    = $wpdb->get_results( $query, OBJECT );

        if( ! empty( $results ) ){
            return true;
        }

        return false;
    }

    public function getFiltersRelatedWithPostType( $postType, $entityName = '')
    {
        global $wpdb;

        $container  = Container::instance();

        // There shouldn't be duplicates in entities
        // Because of validation
        $sql[] = "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_title, {$wpdb->posts}.post_content, {$wpdb->posts}.post_excerpt";
        $sql[] = "FROM {$wpdb->posts}";
        $sql[] = "WHERE {$wpdb->posts}.post_type = '%s'";
        $sql[] = "AND {$wpdb->posts}.post_status = 'publish'";

        $sql[] = "AND {$wpdb->posts}.post_parent IN (";
        $sql[] = "SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}";
        $sql[] = "WHERE {$wpdb->posts}.post_type = '%s'";
        $sql[] = "AND {$wpdb->posts}.post_status = 'publish'";
        $sql[] = "AND {$wpdb->posts}.post_excerpt = '%s'"; // $postType
        $sql[] = ")";
        $sql[] = "ORDER BY {$wpdb->posts}.ID ASC";

        $sql = implode(' ', $sql);

        $query = $wpdb->prepare($sql, FLRT_FILTERS_POST_TYPE, FLRT_FILTERS_SET_POST_TYPE, $postType);

        $key        = $postType.'_get_related_filters';
        $results    = $container->getParam( $key );

        if( ! $results ){
            $results    = $wpdb->get_results( $query, OBJECT );
            if( ! $results ){
                $results = '-1';
            }
            $container->storeParam( $key, $results );
        }

        if( $results === '-1' ){
            return [];
        }

        $filters = [];
        foreach ( $results  as $result ) {
            $filterData = maybe_unserialize( $result->post_content );

            if( ! is_array( $filterData ) ){
                continue;
            }

            if( $entityName && mb_strpos( $result->post_excerpt, $entityName ) === false ){
                continue;
            }

            if( isset( $filters[ $filterData['e_name'] ] ) ){
                continue;
            }

            $filters[$filterData['e_name']]['ID'] = $result->ID;
            $filters[$filterData['e_name']]['label'] = $result->post_title;

            $filters[$filterData['e_name']] = array_merge( $filters[$filterData['e_name']], $filterData );
        }

        return $filters;
    }

    private function makeFiltersQuery()
    {
        $transient_key = 'wpc_filters_query';
        if ( false === ( $results = flrt_get_transient( $transient_key ) ) ) {
            global $wpdb;

            $sql[] = "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_title, {$wpdb->posts}.post_content,";
            $sql[] = "{$wpdb->posts}.post_name, {$wpdb->posts}.post_parent, {$wpdb->posts}.menu_order";
            $sql[] = "FROM {$wpdb->posts}";
            $sql[] = "WHERE {$wpdb->posts}.post_type = '%s'";
            $sql[] = "AND {$wpdb->posts}.post_status = 'publish'";
            $sql[] = "AND {$wpdb->posts}.post_parent != 0";
            $sql[] = "ORDER BY {$wpdb->posts}.menu_order ASC, {$wpdb->posts}.ID DESC";

            $sql = implode(' ', $sql);

            $query      = $wpdb->prepare( $sql, FLRT_FILTERS_POST_TYPE );
            $results    = $wpdb->get_results( $query, OBJECT );

            flrt_set_transient( $transient_key, $results, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS );
        }

        if( ! $results ){
            return [];
        }

        return $results;
    }

    public function getAllConfiguredFilters()
    {
        $key = 'wpc_filters';

        if( ! $filters = $this->getData( $key ) ){
            $filters = [];

            foreach( $this->makeFiltersQuery() as $k => $filter_post ){
                $filter = $this->prepareFilter( $filter_post );
                $k = $filter['e_name'].'_'.$filter['parent'];
                $filters[$k] = $filter;
            }

            $this->storeData( $key, $filters );
        }

        return $filters;
    }

    public function selectOnlySetFilters( $set_id, $keys = [] )
    {
        if( ! $set_id ){
            return false;
        }
        $setFilters = [];

        foreach( $this->allConfiguredFilters as $filter ){
            if( $filter['parent'] == $set_id ){
                if( ! empty( $keys ) ){
                    $setFilters[ $filter['ID'] ] = flrt_extract_vars( $filter, $keys );
                }else{
                    $setFilters[ $filter['ID'] ] = $filter;
                }
            }
        }

        return $setFilters;
    }
    /**
     * @return array
    */
    public function getParamFromFilters( $filters, $key )
    {
        $result = [];
        if( ! is_array( $filters ) ){
            return $result;
        }

        foreach ( $filters as $_key => $_filter ){
            if( isset( $_filter[ $key ] ) ){
                $result[] = $_filter[ $key ];
            }
        }

        return $result;
    }

    public function getConfiguredPathSlugs( $filters = NULL )
    {
        $slugs = [];

        if( ! $filters ){
            $filters = $this->allConfiguredFilters;
        }

        foreach( $filters as $filter ){
            if( $filter['in_path'] === 'yes' ){
                $slugs[] = $filter['slug'];
            }
        }

        return $slugs;
    }

    public function getConfiguredQuerySlugs( $filters = NULL )
    {
        $slugs = [];

        if( ! $filters ){
            $filters = $this->allConfiguredFilters;
        }

        foreach( $filters as $filter ){

            if( FLRT_PERMALINKS_ENABLED ) {
                if ($filter['in_path'] === 'no') {
                    $slugs[] = $filter['slug'];
                }
            }else{
                $slugs[] = $filter['slug'];
            }
        }

        return array_unique( $slugs );
    }

    public function prepareFilter( $filter_post )
    {
        if( ! isset( $filter_post->ID ) ){
            return false;
        }

        $raw_data       = (array) maybe_unserialize( $filter_post->post_content );
        $fse            = Container::instance()->getFilterService();
        $empty_filter   = $fse->getEmptyFilter();

        $defaults = array(
            'ID'         => $filter_post->ID,
            'parent'     => $filter_post->post_parent,
            'menu_order' => $filter_post->menu_order,
            'label'      => $filter_post->post_title,
            'slug'       => $filter_post->post_name
        );

        $filter = wp_parse_args( $raw_data, wp_parse_args( $defaults, $empty_filter ) );
        $filter = apply_filters( 'wpc_after_get_filter', $filter );

        unset($fse);

        return $filter;
    }

    /**
     * @param $sets
     * @return array
     */
    public function getOnlyBelongsFilters( $sets )
    {
        $relevantFilters = [];

        if ( is_array( $sets ) ) {
            foreach ( $sets as $set ) {
                $relevantFilters[] = $this->selectOnlySetFilters( $set['ID'] );
            }
        }

        $relevantFilters = flrt_remove_level_array($relevantFilters);

        return $this->extractBelongsFilters($relevantFilters);
    }

    public function checkForbiddenFilters( $queriedFilters, $allowedFilters )
    {
        $requestedSlugs = $this->getParamFromFilters( $queriedFilters, 'slug' );
        $allowedSlugs   = $this->getParamFromFilters( $allowedFilters, 'slug' );

        foreach( $requestedSlugs as $slug ){
            if( ! in_array( $slug, $allowedSlugs ) ){
                return false;
            }
        }

        return true;
    }

    public function getEntityAllTermsSlugs( $slug )
    {
        $slugs = [];
        $terms = $this->getEntityTermsBySlug( $slug );

        if( is_wp_error( $terms ) ){
            return $slugs;
        }

        foreach( $terms as $k => $termObject ){
            $slugs[ $termObject->term_id ] = $termObject->slug;
        }

        return $slugs;
    }

    /**
     * @param array $sets
     * @return array|false|mixed
     */
    public function getSetsRelatedFilters( $sets = [] )
    {
        /**
         * @todo we have to change this. Not page related, but Set related filters.
         */
        $subkey     = '';
        $wpManager  = Container::instance()->getWpManager();

        if ( empty( $sets ) ) {
            $sets = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
        }

        if ( ! empty( $sets ) ) {
            foreach ( $sets as $set ) {
                $subkey .= '_' . $set['ID'];
            }
        }

        $key = 'wpc_related_filters'.$subkey;

        if ( ! $actual = Container::instance()->getParam( $key ) ) {

            $queried  = $wpManager->getQueryVar( 'queried_values', [] );
            $configured = $this->getOnlyBelongsFilters( $sets );
            $actual     = $configured;

            /**
             * @feature Create new method that populates filters with requested values
             * This should be there in EntityMananger because for all entities it should be done in the same way
             */
            foreach ( $configured as $k => $filter ) {
                // Merge with queried values
                $values = isset( $queried[$filter['slug']]['values'] ) ? $queried[$filter['slug']]['values'] : [];
                $actual[$k]['values'] = $values;
            }

            Container::instance()->storeParam( $key, $actual );
        }

        return $actual;
    }

    /**
     * Should always return postIDs
     */
    public function getAlreadyFilteredPostIds( $setId, $exceptEntity = false )
    {
        /**
         * @bug searching of all queried post IDs doesn't work properly for two or more PostMetaNum filters in one set.
         */

        $wpManager              = Container::instance()->getWpManager();
        $allWpQueriedPostIds    = $this->getAllSetWpQueriedPostIds( $setId );

        $postIds                = $allWpQueriedPostIds ? $allWpQueriedPostIds : [];

        if( $wpManager->getQueryVar('wpc_is_filter_request') ){
            $filteredPostsIdsKeys        = $this->collectFilteredPostsIds( $setId );

            $allWpQueriedPostIdsKeys = array_flip( $allWpQueriedPostIds );
            $allWpQueriedPostIdsKeys = apply_filters( 'wpc_from_products_to_variations', $allWpQueriedPostIdsKeys );

            if( $exceptEntity ){
                unset( $filteredPostsIdsKeys[$exceptEntity->getName()] );
            }

            if( ! empty( $filteredPostsIdsKeys ) ) {
                $intersection_keys = $this->getBetweenFiltersIntersect($filteredPostsIdsKeys, $allWpQueriedPostIdsKeys);

                // Replace back from Variation IDs to Product IDs
                $postIds = apply_filters( 'wpc_from_variations_to_products', array_flip($intersection_keys) );
            }
        }

        return $postIds;
    }

    public function getAllSetWpQueriedPostIds( $setId )
    {
        $ids = [];
        if( ! $setId ){
            return $ids;
        }

        $wpManager  = Container::instance()->getWpManager();
        $key        = 'wpc_all_set_queried_post_ids_' . $setId;
        if ( isset( $_GET['srch'] ) && $_GET['srch'] ) {
            $keyword = filter_input( INPUT_GET, 'srch', FILTER_SANITIZE_SPECIAL_CHARS );
            $key .= '_' . $keyword;
        }
        $ids        = $wpManager->getQueryVar( $key );

        $query_on_the_page = true;

        if ( $ids === false ) {

            // Let's check if the set is related with a query on the page
            $sets             = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
            foreach ( $sets as $set ){
                if( isset( $set['query_on_the_page'] ) && $set['query_on_the_page'] === false ){
                    $query_on_the_page = false;
                }
            }

            $set_filter_query = $wpManager->getQueryVar( 'wpc_set_filter_query_' . $setId );

            if( ! $set_filter_query ){
                /**
                 * string
                 */
                $theGet         = Container::instance()->getTheGet();
                $savedQueryVars = get_post_meta( $setId, 'wpc_filter_set_query_vars', true );

                if( $savedQueryVars ){
                    $query_vars = maybe_unserialize( $savedQueryVars );

                    if( is_array( $query_vars ) ){
                        $set_filter_query = new \WP_Query();

                        if( $query_on_the_page ){
                            if( isset( $theGet['s'] ) ){
                                $query_vars['s'] = $theGet['s'];
                            }
                        }

                        $set_filter_query->query_vars = $query_vars;
                    }
                }
            }


            $detected_source = '';
            if (!empty($set_filter_query->query_vars['flrt_detected_source'])) {
                $detected_source = $set_filter_query->query_vars['flrt_detected_source'];
            }

            if ($set_filter_query instanceof \WP_Query) {

                // Configure WP_Query object to select only posts IDs
                $set_filter_query->set( 'fields', 'ids' );
                $set_filter_query->set( 'posts_per_page', -1 );
                $set_filter_query->set( 'nopaging', true );
                $set_filter_query->set( 'post_status', 'publish' );
                $set_filter_query->set( 'flrt_query_clone', true );

                do_action( 'wpc_all_set_wp_queried_posts' , $set_filter_query, $setId );

                $ids = $set_filter_query->get_posts();

            }

            if(!empty($detected_source) && isset($set_filter_query->query_vars['flrt_detected_source'])){
                $set_filter_query->query_vars['flrt_detected_source'] = $detected_source;
            }

            $ids = apply_filters( 'wpc_check_errors_ids', $ids, $set_filter_query);

            $ids = (! empty( $ids ) ) ? $ids : [];

            $wpManager->setQueryVar($key, $ids);

            unset($set_filter_query);
        }

        return $ids;
    }

    // This method must be executed before output
    public function prepareEntitiesToDisplay( $sets )
    {
        $container  = Container::instance();
        $wpManager  = $container->getWpManager();
        $subkey     = '';

        $post_type      = $sets[0]['filtered_post_type'];
        $setId          = $sets[0]['ID'];
        $current_set    = $sets[0];

        $all_sets         = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
        $queryRelatedSets = flrt_get_sets_with_the_same_query( $all_sets, $current_set );

        $filter_by_stock_exists = false;

        $relatedSets = [];
        foreach ( $queryRelatedSets as $set_id ) {
            $relatedSets[] = array( 'ID' => $set_id );
        }

        $subkey = implode( '_', $queryRelatedSets );
        $key    = 'wpc_entities_prepared_' . $subkey;

        if( ! $container->getParam( $key ) ) {

            $relatedFilters = $this->getSetsRelatedFilters( $relatedSets );

            if ( $post_type === 'product' && ! empty( $relatedFilters )  ) {
                foreach ($relatedFilters as $filter) {
                    if (isset($filter['e_name']) && $filter['e_name'] === '_stock_status') {
                        $filter_by_stock_exists = true;
                        break;
                    }
                }
            }

            $relatedFilters = apply_filters( 'wpc_related_filters_before_terms_count', $relatedFilters, $sets );

            $allPostsIds = $this->getAllSetWpQueriedPostIds( $setId );

            if ( ! empty( $allPostsIds ) ) {
                $allPostsIds = array_flip( $allPostsIds );
            }

            $allEntities = [];

            if ( ! empty( $relatedFilters ) ) {

                foreach ( $relatedFilters as $filter ) {
                    // Part 1 collect all entities
                    $entity = $this->getEntityByFilter( $filter, $post_type );

                    if ( $entity instanceof PostMetaExistsEntity ) {
                        $entity->setPostTypes( array( $post_type ) );
                    }

                    $entity->populateTermsWithPostIds( $filter['parent'], $post_type );

                    $allEntities[$entity->getName()] = $entity;
                }
            }

            // Post IDs with variations instead of parent products
            // This must be called here, after the $entity->populateTermsWithPostIds(); method called
            $filteredAllPostsIds = $this->collectFilteredPostsIds( $setId );

            /**
             * Allows to modify filtered post ids
             */
            $filteredAllPostsIds = apply_filters( 'wpc_filtered_all_posts_before_terms_count', $filteredAllPostsIds, $allEntities );

            /**
             * Allows to modify all post ids
             */
            $allPostsIds         = apply_filters( 'wpc_from_products_to_variations', $allPostsIds );

            foreach ( $allEntities as $entityName => $entity ) {
                if ( $entityName === '_stock_status' && ! $filter_by_stock_exists && $post_type === 'product' ) {
                    $filter = flrt_get_stock_status_filter_emulation();
                } else {
                    $filter = $this->getFilterByEname( $entityName, $relatedSets );
                }
                foreach ( $entity->items as $index => $term ) {
                    $entity->items[$index]->count = count( $entity->items[$index]->posts );
                }

                if ( $entity instanceof PostMetaNumEntity || $entity instanceof TaxonomyNumEntity || $entity instanceof PostDateEntity || $entity instanceof PostMetaDateEntity ) {
                    $postsIn = apply_filters( 'wpc_min_and_max_values_numeric_filters', $this->getAlreadyFilteredPostIds( $setId, $entity ), $entity );
                    $entity->updateMinAndMaxValues( $postsIn );
                }

                if ( in_array( $filter['orderby'], ['menuasc', 'menudesc'] ) ) {
                    if( $entity instanceof TaxonomyEntity ){
                        foreach ($entity->items as $k => $term) {
                            $termOrder = get_term_meta( $term->term_id, 'order', true );
                            $term->menu_order   = $termOrder ? $termOrder : 0;
                            $entity->items[$k]  = $term;
                        }
                    }
                }

                $is_rating = (isset($filter['view']) && $filter['view'] === 'rating' && $filter['e_name'] === 'product_visibility' ) ? true : false;
                if($is_rating){
                    $filter['orderby'] = 'default';
                }

                if( $filter['orderby'] === 'default' ) {
                    $entity->items = apply_filters( 'wpc_default_sorting_terms', $entity->items, $filter );
                } else {
                    $entity->items = $this->sortTerms( $entity->items, $filter['orderby'] );
                }

                /**
                 * @feature move selected terms to top
                 */
                $used_for_variations = isset( $filter['used_for_variations'] ) ? $filter['used_for_variations'] : false;
                $entity->items = apply_filters( 'wpc_items_before_calc_term_count', $entity->items, $entity, $used_for_variations );

                foreach ( $entity->items as $index => $term ) {
                    $entity->items[$index]->cross_count = $this->calcTermCount( array_flip($term->posts), $filteredAllPostsIds, $allPostsIds, $filter );
                }
            }

            $container->storeParam( $key, true );
        }
    }

    /**
     * @param array $terms list of terms
     * @param string $sortby
     * @return array sorted list of terms
     */
    private function sortTerms( $terms, $sortby )
    {
        if( ! $sortby ){
            $sortby = 'nameasc';
        }

        switch( $sortby ){
            case 'nameasc':
                usort( $terms, self::compareAsc('name') );
                break;
            case 'postcountasc':
                usort( $terms, self::compareAsc('count') );
                break;
            case 'idasc':
                usort( $terms, self::compareAsc('term_id') );
                break;
            case 'menuasc':
                usort( $terms, self::compareAsc('menu_order') );
                break;
            case 'namedesc':
                usort( $terms, self::compareDesc('name') );
                break;
            case 'postcountdesc':
                usort( $terms, self::compareDesc('count') );
                break;
            case 'iddesc':
                usort( $terms, self::compareDesc('term_id') );
                break;
            case 'menudesc':
                usort( $terms, self::compareDesc('menu_order') );
                break;
        }

        return $terms;
    }

    public static function compareAsc( $key ){

        return function ($a, $b) use ($key) {
            $value_1 = isset( $a->$key ) ? strtolower($a->$key) : 0;
            $value_2 = isset( $b->$key ) ? strtolower($b->$key) : 0;

            if ($value_1 == $value_2) {
                return 0;
            }

            return ($value_1 > $value_2) ? +1 : -1;
        };
    }

    public static function compareDesc( $key ){
        return function ($a, $b) use ($key) {
            $value_1 = isset( $a->$key ) ? strtolower($a->$key) : 0;
            $value_2 = isset( $b->$key ) ? strtolower($b->$key) : 0;

            if ($value_1 == $value_2) {
                return 0;
            }

            return ($value_1 < $value_2) ? +1 : -1;
        };
    }

    public function calcTermCount( $termPostsIds, $filteredPostsIds, $allPostsIds, $filter )
    {
        if( empty( $termPostsIds ) ){
            return 0;
        }

        $e_name = $filter['e_name'];
        $logic  = $filter['logic'];

        if( ! isset( $filteredPostsIds[$e_name] ) ){
            $filteredPostsIds[$e_name] = [];
        }

        // Intersection for logic OR between filter terms
        if ( $logic === 'or' ) {

            $filteredPostsIds[$e_name] += $termPostsIds;

        // Intersection for logic AND between filter terms
        } elseif ( $logic === 'and' ) {
            if( ! empty( $filteredPostsIds[$e_name] ) ){
                $filteredPostsIds[$e_name] = array_intersect_key( $allPostsIds, $filteredPostsIds[$e_name], $termPostsIds );
            }else{
                $filteredPostsIds[$e_name] = array_intersect_key( $allPostsIds, $termPostsIds );
            }
        }

        $betweenFiltersIntersect = $this->getBetweenFiltersIntersect( $filteredPostsIds, $allPostsIds );
        $finalInterSection = apply_filters( 'wpc_from_variations_to_products', array_flip( array_intersect_key( $betweenFiltersIntersect, $termPostsIds ) ) );

        return count( $finalInterSection );
    }

    public function getBetweenFiltersIntersect( $filteredPostsIds, $allPostsIds )
    {
        $betweenFiltersAND = [];
        /**
         * @bug when intersection is already empty, this count another intersection as more than 0
         * example - http://filter.stepasyuk.com/shop/category-hoodies/color-red/alphabet-alpha/
         * when there are hidden product, counter also doesn't work properly
         * example - http://filter.stepasyuk.com/shop/category-music/
         */

        // This implements logic AND between separate filters
        if ( ! empty( $filteredPostsIds ) ) {
            $i = 1;
            foreach ( $filteredPostsIds as $e_name => $singleFilterPostsIds ) {
                if( $i !== 1 ) {
                    $betweenFiltersAND = array_intersect_key( $allPostsIds, $betweenFiltersAND, $singleFilterPostsIds );
                } else {
                    $betweenFiltersAND = array_intersect_key( $allPostsIds, $singleFilterPostsIds );
                }
                $i++;
            }
        }

        // Final intersect between All posts and Filtered
        return array_intersect_key( $allPostsIds, $betweenFiltersAND );
    }

    public function getTaxonomyTermsForDropdown( $taxonomyName, $optionGroup = false )
    {
        $terms = [];
        if( ! $taxonomyName ){
            return $terms;
        }

        $filter['e_name'] = $taxonomyName;
        $filter['entity'] = 'taxonomy';
        $entity = $this->getEntityByFilter( $filter );

        if( ! $entity ){
            return $terms;
        }

        return $entity->getTermsForSelect( $optionGroup );
    }

    public function getAuthorTermsForDropdown( $optionGroup = false )
    {
        $args = array(
            'has_published_posts' => true,
            'orderby'             => 'display_name'
        );

        $key = 'wpc_users';
        if( $optionGroup ){
            $key .= '_group';
        }

        if( ! $authors = $this->getData( $key ) ) {

            $authors = [];

            $users = get_users($args);

            foreach ($users as $user) {
                if ($optionGroup) {
                    $authors['author:' . $user->ID] = $user->data->display_name;
                } else {
                    $authors[$user->ID] = $user->data->display_name;
                }
            }

            $this->storeData( $key, $authors );
        }

        return $authors;
    }

    public function safeExplodeFilterValues($params, $slug, $sep, $explode = true)
    {
        if (!is_string($params)) {
            $params = (string) $params;
        }

        if ($sep === '') {
            return $explode ? [$params] : $params;
        }

        // Get all term slugs for the given filter slug
        $allEntityTerms = $this->getEntityAllTermsSlugs($slug);

        // Extract pure values (slugs)
        $terms = array_values($allEntityTerms);

        // Skip empty terms
        $terms = array_filter($terms, static function ($t) {
            return $t !== '';
        });

        // Keep only terms that contain the separator
        $terms = array_filter($terms, static function ($t) use ($sep) {
            return mb_strpos($t, $sep) !== false;
        });

        // Sort by length in descending order to protect longer terms first
        usort($terms, static function ($a, $b) {
            return mb_strlen($b) <=> mb_strlen($a);
        });

        // Replace the separator inside matched terms with a placeholder
        // so explode() won't split inside a single term
        foreach ($terms as $entityTerm) {
            $escapedTerm = preg_quote($entityTerm, '/');
            $pattern = "/$escapedTerm/u";

            $params = preg_replace_callback($pattern, function ($matches) use ($sep) {
                // Replace internal separators only inside the matched term
                return str_replace($sep, '#', $matches[0]);
            }, $params);
        }

        // Finally, split by the separator if requested
        if ($explode) {
            return explode($sep, $params);
        }

        return $params;
    }



    public function safeImplodeFilterValues( $filterValues, $sep )
    {
        if( ! is_array( $filterValues ) ){
            return [];
        }
        // Replace back
        array_walk($filterValues, function (&$value) use ($sep) {
            $value = str_replace('#', $sep, $value);
        });

        return $filterValues;
    }

    /**
     * @return array List of all queried posts or empty array
     */
    public function collectFilteredPostsIds( $setId )
    {
        $wpManager      = Container::instance()->getWpManager();
        $fss            = Container::instance()->getFilterSetService();

        $theSet         = $fss->getSet( $setId );
        $postType       = isset( $theSet['post_type']['value'] ) ? $theSet['post_type']['value'] : '';
        $filtered_wp_query  = isset( $theSet['wp_filter_query'] ) ? $theSet['wp_filter_query']: '-1';

        $current_set    = array(
            'ID'                 => $setId,
            'filtered_post_type' => $postType,
            'query'              => $filtered_wp_query
        );

        $allSetPostsIds = $this->getAllSetWpQueriedPostIds( $setId );
        $queriedFilters = $wpManager->getQueryVar('queried_values');

        if( $allSetPostsIds ){
            $allSetPostsIds = array_flip( $allSetPostsIds );
        }

        $allSetPostsIds = apply_filters( 'wpc_from_products_to_variations', $allSetPostsIds );

        if ( ! $wpManager->getQueryVar('wpc_is_filter_request') ) {
            return [];
        }

        $queriedAllPosts = [];

        $all_sets = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
        $queryRelatedSets = flrt_get_sets_with_the_same_query( $all_sets, $current_set );

        $set_filter_keys = $this->getSetFilterKeys( $queryRelatedSets );

        foreach ( $queriedFilters as $slug => $queriedFilter ) {

            $queried_value_key = $queriedFilter['entity'].'#'.$queriedFilter['e_name'];
            $do_filter_request = apply_filters( 'wpc_do_filter_request', true, $queriedFilter, null );

            if( ! in_array( $queried_value_key, $set_filter_keys ) || ! $do_filter_request ) {
                continue;
            }

            $entity = $this->getEntityByFilter( $queriedFilter, $postType );
            $e_name = $queriedFilter['e_name'];
            $queriedAllPosts[$e_name] = [];

            // Allows to replace product IDs with their variation IDs
            $entity->items = apply_filters( 'wpc_items_before_calc_term_count', $entity->items, $entity, $queriedFilter['used_for_variations'] );
            foreach ( $entity->items as $term ) {

                if ( ! isset( $term->posts ) ) {
                    continue;
                }

                if ( in_array( $queriedFilter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
                    $doCalculate = in_array( $term->slug, array_keys( $queriedFilter['values'] ) );
                } else {
                    $doCalculate = in_array( $term->slug, $queriedFilter['values'] );
                }

                if ( $doCalculate ) {

                    // Intersection for logic OR between filter terms
                    if ( $queriedFilter['logic'] === 'or' ) {

                        $queriedAllPosts[$e_name] += array_flip( $term->posts );

                    // Intersection for logic AND between filter terms
                    } elseif ( $queriedFilter['logic'] === 'and' ) {

                        if( ! empty( $queriedAllPosts[$e_name] ) ){
                            $queriedAllPosts[$e_name] = array_intersect_key( $allSetPostsIds, $queriedAllPosts[$e_name], array_flip($term->posts) );
                        } else {
                            $queriedAllPosts[$e_name] = array_intersect_key( $allSetPostsIds, array_flip($term->posts) );
                        }
                    }
                }
            }
        }

        return apply_filters( 'wpc_queried_all_posts', $queriedAllPosts, $allSetPostsIds, $queriedFilters, $setId );
    }

    public function getSetFilterKeys( $setIds )
    {
        $set_filter_keys = [];

        if( ! $setIds || empty( $setIds )){
            return $set_filter_keys;
        }

        $sets = [];
        foreach( $setIds as $setId ){
            $sets[] = array( 'ID' => $setId );
        }

        $set_filters     = $this->getSetsRelatedFilters( $sets );

        foreach ( $set_filters as $filter ){
            $set_filter_keys[] = $filter['entity'].'#'.$filter['e_name'];
        }

        return $set_filter_keys;
    }

    public function getEntityTermsBySlug( $slug )
    {
        // Slug is not unique param, but it's ok for this case.
        $filterEntity   = $this->getFilterBySlug( $slug /*, array( 'entity', 'e_name' )*/ );
        $entity         = $this->getEntityByFilter( $filterEntity );

        return $entity->getAllExistingTerms();
    }

    public function addTermsToWpQuery( $queried_value, $wp_query )
    {
        $entity = $this->getEntityByFilter( $queried_value );
        return $entity->addTermsToWpQuery( $queried_value, $wp_query );
    }
}