<?php
namespace PowerpackElements\Classes;

use PowerpackElements\Modules\Posts\Module;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Class PP_Posts_Query_Builder.
 *
 * Builds WP_Query arguments from widget settings. Shared by Posts_Base and
 * any non-posts widgets that expose a "posts" data source.
 *
 * @since 2.13.0
 */
class PP_Posts_Query_Builder {

	/**
	 * Query var carrying an offset that must not break pagination.
	 *
	 * Prefixed so that other plugins using the same "offset_to_fix" convention
	 * do not process our queries — and we do not process theirs. Sharing the
	 * name meant the offset was subtracted from found_posts once per plugin.
	 *
	 * @since 3.0.1
	 */
	const OFFSET_QUERY_VAR = 'pp_offset_to_fix';

	/**
	 * Run a query built by this class.
	 *
	 * An offset combined with pagination breaks WP_Query: the offset is applied
	 * to every page, so page two starts in the wrong place and the found-posts
	 * count is too high. build() therefore emits OFFSET_QUERY_VAR instead of a
	 * plain offset, and this method converts it.
	 *
	 * The two hooks live only for the duration of the query that needs them, so
	 * nothing else on the page — the main query, the theme, another plugin — is
	 * touched, and the offset is discounted from found_posts exactly once.
	 *
	 * @since 3.0.1
	 *
	 * @param array $query_args WP_Query arguments, typically from build().
	 *
	 * @return \WP_Query
	 */
	public static function run( $query_args ) {
		if ( empty( $query_args[ self::OFFSET_QUERY_VAR ] ) ) {
			return new \WP_Query( $query_args );
		}

		add_action( 'pre_get_posts', [ __CLASS__, 'fix_query_offset' ], 1 );
		add_filter( 'found_posts', [ __CLASS__, 'fix_query_found_posts' ], 1, 2 );

		$query = new \WP_Query( $query_args );

		remove_action( 'pre_get_posts', [ __CLASS__, 'fix_query_offset' ], 1 );
		remove_filter( 'found_posts', [ __CLASS__, 'fix_query_found_posts' ], 1 );

		return $query;
	}

	/**
	 * Apply the offset to the current page rather than to every page.
	 *
	 * @since 3.0.1
	 *
	 * @param \WP_Query $query Query instance.
	 *
	 * @return void
	 */
	public static function fix_query_offset( $query ) {
		$offset = $query->get( self::OFFSET_QUERY_VAR );

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

		if ( $query->is_paged ) {
			$query->query_vars['offset'] = $offset + ( ( $query->query_vars['paged'] - 1 ) * $query->query_vars['posts_per_page'] );
		} else {
			$query->query_vars['offset'] = $offset;
		}
	}

	/**
	 * Discount the offset from the found-posts total.
	 *
	 * @since 3.0.1
	 *
	 * @param int       $found_posts Posts found.
	 * @param \WP_Query $query       Query instance.
	 *
	 * @return int
	 */
	public static function fix_query_found_posts( $found_posts, $query ) {
		$offset = $query->get( self::OFFSET_QUERY_VAR );

		if ( $offset ) {
			$found_posts -= $offset;
		}

		return $found_posts;
	}

	/**
	 * Build WP_Query arguments from widget settings.
	 *
	 * @since 2.13.0
	 *
	 * @param array $settings Widget settings (typically from get_settings_for_display()).
	 * @param array $args     {
	 *     Builder options.
	 *
	 *     @type string    $widget_type            Used in the ppe_{widget_type}_query_args filter. Default 'posts'.
	 *     @type string    $filter                 Taxonomy term slug for runtime filtering. Default ''.
	 *     @type string    $taxonomy_filter        Taxonomy slug paired with $filter. Default ''.
	 *     @type string    $search                 Search string applied as WP_Query 's'. Default ''.
	 *     @type bool      $all_posts              If true, posts_per_page is left at -1. Default false.
	 *     @type mixed     $paged                  Value for WP_Query 'paged'. Default ''.
	 *     @type bool      $old_code               Legacy category/tag control-name compatibility. Default false.
	 *     @type string    $posts_count_var        Settings key holding posts-per-page (e.g. 'posts_count'). Default ''.
	 *     @type int       $posts_count            Explicit posts-per-page override. Default 0.
	 *     @type \WP_Query $related_fallback_query Current widget query instance, used for the
	 *                                             related-posts "recent" fallback. Default null.
	 *     @type array     $posts_filter_keys      Per-post-type overrides for the include/exclude
	 *                                             posts-filter settings key. Example:
	 *                                             [ 'post' => 'exclude_posts' ]. Default [].
	 *     @type bool      $no_found_rows          Skip the found-rows count. Set this when the widget
	 *                                             never reads max_num_pages — it saves WP_Query the
	 *                                             SQL_CALC_FOUND_ROWS work and the FOUND_ROWS() round
	 *                                             trip. Ignored for the main query. Default false.
	 * }
	 * @return array WP_Query arguments, already passed through the ppe_{widget_type}_query_args filter.
	 */
	public static function build( $settings, $args = [] ) {
		$args = wp_parse_args( $args, [
			'widget_type'            => 'posts',
			'filter'                 => '',
			'taxonomy_filter'        => '',
			'search'                 => '',
			'all_posts'              => false,
			'paged'                  => '',
			'old_code'               => false,
			'posts_count_var'        => '',
			'posts_count'            => 0,
			'related_fallback_query' => null,
			'posts_filter_keys'      => [],
			'no_found_rows'          => false,
		] );

		$widget_type     = $args['widget_type'];
		$filter          = $args['filter'];
		$taxonomy_filter = $args['taxonomy_filter'];
		$search          = $args['search'];
		$all_posts       = (bool) $args['all_posts'];
		$paged           = $args['paged'];
		$old_code        = (bool) $args['old_code'];
		$posts_count_var = $args['posts_count_var'];
		$posts_count     = $args['posts_count'];

		$tax_count    = 0;
		$post__not_in = [];

		/*
		 * Not every consumer registers the whole Query section — a widget may
		 * expose a posts source without the main-query, exclude-current or
		 * avoid-duplicates controls — so these are read defensively.
		 */
		$settings = wp_parse_args( $settings, [
			'query_type'       => 'custom',
			'exclude_current'  => '',
			'avoid_duplicates' => '',
		] );

		if ( 'main' === $settings['query_type'] ) {
			$current_query_vars = $GLOBALS['wp_query']->query_vars;
			return apply_filters( "ppe_{$widget_type}_query_args", $current_query_vars, $settings );
		}

		$query_args = [
			'post_status'         => [ 'publish' ],
			'orderby'             => $settings['orderby'],
			'order'               => $settings['order'],
			'ignore_sticky_posts' => ( 'yes' === $settings['sticky_posts'] ) ? 0 : 1,
			'posts_per_page'      => -1,
		];

		if ( 'attachment' === $settings['post_type'] ) {
			$query_args['post_status'] = [ 'inherit' ];
		}

		if ( ! $posts_count ) {
			$posts_per_page = ( $posts_count_var ) ? $settings[ $posts_count_var ] : ( isset( $settings['posts_per_page'] ) ? $settings['posts_per_page'] : '' );
		} else {
			$posts_per_page = $posts_count;
		}

		if ( ! $all_posts ) {
			$query_args['posts_per_page'] = $posts_per_page;
		}

		if ( 'related' === $settings['post_type'] ) {

			$related_terms = $settings['related_filter_include'];
			$post_terms    = wp_get_object_terms( get_the_ID(), $settings['related_filter_include'], [ 'fields' => 'ids' ] );

			// Query Arguments.
			$query_args['post_type'] = get_post_type();

			if ( ! empty( $settings['related_include_by'] ) ) {
				if ( in_array( 'authors', $settings['related_include_by'], true ) ) {
					$query_args['author'] = get_the_author_meta( 'ID' );
				}

				if ( in_array( 'terms', $settings['related_include_by'], true ) ) {
					if ( ! empty( $related_terms ) && ! is_wp_error( $related_terms ) ) {

						foreach ( $related_terms as $index => $tax ) {

							$query_args['tax_query'][] = [
								'taxonomy' => $tax,
								'field'    => 'term_id',
								'terms'    => $post_terms,
							];

						}
					}
				}
			}

			if ( ! empty( $settings['related_exclude_by'] ) ) {
				if ( in_array( 'current_post', $settings['related_exclude_by'], true ) ) {
					$post__not_in = [ get_the_ID() ];
				}

				if ( in_array( 'authors', $settings['related_exclude_by'], true ) ) {
					$query_args['author'] = '-' . get_the_author_meta( 'ID' );
				}
			}

			if ( 'recent' === $settings['related_fallback'] ) {
				$fallback_query = $args['related_fallback_query'];

				if ( ! $fallback_query->found_posts ) {
					$query_args = [
						'post_status'         => [ 'publish' ],
						'post_type'           => get_post_type(),
						'orderby'             => $settings['orderby'],
						'order'               => $settings['order'],
						'ignore_sticky_posts' => ( 'yes' === $settings['sticky_posts'] ) ? 0 : 1,
						'showposts'           => $posts_per_page,
					];
				}
			}
		} else {

			// Query Arguments.
			$query_args['post_type'] = $settings['post_type'];
			if ( 0 < $settings['offset'] ) {

				/**
				 * Offset break the pagination. Using WordPress's work around
				 *
				 * @see https://codex.wordpress.org/Making_Custom_Queries_using_Offset_and_Pagination
				 */
				$query_args[ self::OFFSET_QUERY_VAR ] = $settings['offset'];
			}
			$query_args['paged'] = $paged;

			// Author Filter.
			if ( ! empty( $settings['authors'] ) ) {
				$query_args[ $settings['author_filter_type'] ] = $settings['authors'];
			}

			// Posts Filter.
			$post_type = $settings['post_type'];

			$posts_filter_key = isset( $args['posts_filter_keys'][ $post_type ] )
				? $args['posts_filter_keys'][ $post_type ]
				: $post_type . '_filter';

			if ( ! empty( $settings[ $posts_filter_key ] ) ) {
				if ( 'post__not_in' === $settings[ $post_type . '_filter_type' ] ) {
					$post__not_in = $settings[ $posts_filter_key ];
				} else {
					$query_args[ $settings[ $post_type . '_filter_type' ] ] = $settings[ $posts_filter_key ];
				}
			}

			// Taxonomy Filter.
			$taxonomy = PP_Posts_Helper::get_post_taxonomies( $post_type );

			$tax_cat_in     = '';
			$tax_cat_not_in = '';
			$tax_tag_in     = '';
			$tax_tag_not_in = '';

			if ( ! empty( $taxonomy ) && ! is_wp_error( $taxonomy ) ) {

				foreach ( $taxonomy as $index => $tax ) {

					$tax_control_key = $index . '_' . $post_type;

					if ( $old_code ) {
						if ( 'post' === $post_type ) {
							if ( 'post_tag' === $index ) {
								$tax_control_key = 'tags';
							} elseif ( 'category' === $index ) {
								$tax_control_key = 'categories';
							}
						}
					}

					if ( ! empty( $settings[ $tax_control_key ] ) ) {

						$operator = $settings[ $index . '_' . $post_type . '_filter_type' ];

						$query_args['tax_query'][] = [
							'taxonomy' => $index,
							'field'    => 'term_id',
							'terms'    => $settings[ $tax_control_key ],
							'operator' => $operator,
						];

						switch ( $index ) {
							case 'category':
								if ( 'IN' === $operator ) {
									$tax_cat_in = $settings[ $tax_control_key ];
								} elseif ( 'NOT IN' === $operator ) {
									$tax_cat_not_in = $settings[ $tax_control_key ];
								}
								break;

							case 'post_tag':
								if ( 'IN' === $operator ) {
									$tax_tag_in = $settings[ $tax_control_key ];
								} elseif ( 'NOT IN' === $operator ) {
									$tax_tag_not_in = $settings[ $tax_control_key ];
								}
								break;
						}
					}
				}
			}

			if ( '' !== $filter && '*' !== $filter ) {
				// Taxonomy Filter.
				$taxonomy = PP_Posts_Helper::get_post_taxonomies( $post_type );

				$tax_cat_in     = '';
				$tax_cat_not_in = '';
				$tax_tag_in     = '';
				$tax_tag_not_in = '';

				if ( ! empty( $taxonomy ) && ! is_wp_error( $taxonomy ) ) {

					foreach ( $taxonomy as $index => $tax ) {

						$tax_control_key = $index . '_' . $post_type;

						if ( $old_code ) {
							if ( 'post' === $post_type ) {
								if ( 'post_tag' === $index ) {
									$tax_control_key = 'tags';
								} elseif ( 'category' === $index ) {
									$tax_control_key = 'categories';
								}
							}
						}

						if ( ! empty( $settings[ $tax_control_key ] ) ) {

							$operator = $settings[ $index . '_' . $post_type . '_filter_type' ];

							$query_args['tax_query'][] = [
								'taxonomy' => $index,
								'field'    => 'term_id',
								'terms'    => $settings[ $tax_control_key ],
								'operator' => $operator,
							];

							switch ( $index ) {
								case 'category':
									if ( 'IN' === $operator ) {
										$tax_cat_in = $settings[ $tax_control_key ];
									} elseif ( 'NOT IN' === $operator ) {
										$tax_cat_not_in = $settings[ $tax_control_key ];
									}
									break;

								case 'post_tag':
									if ( 'IN' === $operator ) {
										$tax_tag_in = $settings[ $tax_control_key ];
									} elseif ( 'NOT IN' === $operator ) {
										$tax_tag_not_in = $settings[ $tax_control_key ];
									}
									break;
							}
						}
					}
				}

				$query_args['tax_query'][ $tax_count ]['taxonomy'] = $taxonomy_filter;
				$query_args['tax_query'][ $tax_count ]['field']    = 'slug';
				$query_args['tax_query'][ $tax_count ]['terms']    = $filter;
				$query_args['tax_query'][ $tax_count ]['operator'] = 'IN';
			}

			if ( '' !== $search ) {
				$query_args['s'] = $search;
			}
		}

		if ( 'anytime' !== $settings['select_date'] ) {
			$select_date = $settings['select_date'];
			if ( ! empty( $select_date ) ) {
				$date_query = [];
				if ( 'today' === $select_date ) {
					$date_query['after'] = '-1 day';
				} elseif ( 'week' === $select_date ) {
					$date_query['after'] = '-1 week';
				} elseif ( 'month' === $select_date ) {
					$date_query['after'] = '-1 month';
				} elseif ( 'quarter' === $select_date ) {
					$date_query['after'] = '-3 month';
				} elseif ( 'year' === $select_date ) {
					$date_query['after'] = '-1 year';
				} elseif ( 'exact' === $select_date ) {
					$after_date = $settings['date_after'];
					if ( ! empty( $after_date ) ) {
						$date_query['after'] = $after_date;
					}
					$before_date = $settings['date_before'];
					if ( ! empty( $before_date ) ) {
						$date_query['before'] = $before_date;
					}
					$date_query['inclusive'] = true;
				}

				$query_args['date_query'] = $date_query;
			}
		}

		// Sticky Posts Filter.
		if ( 'yes' === $settings['sticky_posts'] && 'yes' === $settings['all_sticky_posts'] ) {
			$post__in = get_option( 'sticky_posts' );

			$query_args['ignore_sticky_posts'] = 1;
			$query_args['post__in']            = $post__in;
		}

		// Exclude current post.
		if ( 'yes' === $settings['exclude_current'] ) {
			if ( is_singular() ) {
				$query_args['post__not_in'] = [ get_queried_object_id() ];
			}
		}

		if ( 'yes' === $settings['avoid_duplicates'] ) {
			$post__not_in = array_merge( $post__not_in, Module::$displayed_ids );
		}

		if ( ! empty( $post__not_in ) ) {
			$query_args['post__not_in'] = $post__not_in;
		}

		if ( $args['no_found_rows'] ) {
			$query_args['no_found_rows'] = true;
		}

		return apply_filters( "ppe_{$widget_type}_query_args", $query_args, $settings );
	}
}
