<?php

namespace WPML\TM\Jobs\Query;

use \InvalidArgumentException;
use \WPML_TM_Jobs_Search_Params;
use \RuntimeException;


class CompositeQuery implements Query {
	const METHOD_UNION = 'union';
	const METHOD_COUNT = 'count';

	/**
	 * Job queries
	 *
	 * @var Query[]
	 */
	private $queries;

	/**
	 * Limit query helper
	 *
	 * @var LimitQueryHelper
	 */
	private $limit_query_helper;

	/**
	 * Order query helper
	 *
	 * @var OrderQueryHelper
	 */
	private $order_query_helper;

	/**
	 * @param Query[]  $queries      Job queries.
	 * @param LimitQueryHelper $limit_helper Limit helper.
	 * @param OrderQueryHelper $order_helper Order helper.
	 *
	 * @throws InvalidArgumentException In case of error.
	 */
	public function __construct(
		array $queries,
		LimitQueryHelper $limit_helper,
		OrderQueryHelper $order_helper
	) {
		$queries = array_filter( $queries, array( $this, 'is_query_valid' ) );
		if ( empty( $queries ) ) {
			throw new InvalidArgumentException( 'Collection of sub-queries is empty or contains only invalid elements' );
		}

		$this->queries            = $queries;
		$this->limit_query_helper = $limit_helper;
		$this->order_query_helper = $order_helper;
	}


	/**
	 * Get data query
	 *
	 * @param WPML_TM_Jobs_Search_Params $params Job search params.
	 *
	 * @throws InvalidArgumentException In case of error.
	 * @return string
	 */
	public function get_data_query( WPML_TM_Jobs_Search_Params $params ) {
		if ( ! $params->get_job_types() ) {
			// We are merging subqueries here, that's why LIMIT must be applied to final query.
			$params_without_pagination_and_sorting = clone $params;
			$params_without_pagination_and_sorting->set_limit( 0 )->set_offset( 0 );
			$params_without_pagination_and_sorting->set_sorting( array() );

			$query = $this->get_sql( $params_without_pagination_and_sorting, self::METHOD_UNION );

			$order = $this->order_query_helper->get_order( $params );
			if ( $order ) {
				$query .= ' ' . $order;
			}
			$limit = $this->limit_query_helper->get_limit( $params );
			if ( $limit ) {
				$query .= ' ' . $limit;
			}

			return $query;
		} else {
			return $this->get_sql( $params, self::METHOD_UNION );
		}
	}

	/**
	 * Get count query
	 *
	 * @param WPML_TM_Jobs_Search_Params $params Job search params.
	 *
	 * @return int|string
	 */
	public function get_count_query( WPML_TM_Jobs_Search_Params $params ) {
		$params_without_pagination_and_sorting = clone $params;
		$params_without_pagination_and_sorting->set_limit( 0 )->set_offset( 0 );
		$params_without_pagination_and_sorting->set_sorting( array() );

		return $this->get_sql( $params_without_pagination_and_sorting, self::METHOD_COUNT );
	}

	/**
	 * Get SQL request string
	 *
	 * @param WPML_TM_Jobs_Search_Params $params Job search params.
	 * @param string                     $method Query method.
	 *
	 * @throws InvalidArgumentException In case of error.
	 * @throws RuntimeException In case of error.
	 * @return string
	 */
	private function get_sql( WPML_TM_Jobs_Search_Params $params, $method ) {
		switch ( $method ) {
			case self::METHOD_UNION:
				$query_method = 'get_data_query';
				break;
			case self::METHOD_COUNT:
				$query_method = 'get_count_query';
				break;
			default:
				throw new InvalidArgumentException( 'Invalid method argument: ' . $method );
		}

		$parts = array();
		foreach ( $this->queries as $query ) {
			$query_string = $query->$query_method( $params );
			if ( $query_string ) {
				$parts[] = $query_string;
			}
		}

		if ( ! $parts ) {
			throw new RuntimeException( 'None of subqueries matches to specified search parameters' );
		}

		if ( 1 === count( $parts ) ) {
			return current( $parts );
		}

		switch ( $method ) {
			case self::METHOD_UNION:
				return $this->get_union( $parts );
			case self::METHOD_COUNT:
				return $this->get_count( $parts );
		}

		return null;
	}

	/**
	 * Get union
	 *
	 * @param array $parts Query parts.
	 *
	 * @return string
	 */
	private function get_union( array $parts ) {
		return '( ' . implode( ' ) UNION ( ', $parts ) . ' )';
	}

	/**
	 * Get count
	 *
	 * @param array $parts Query parts.
	 *
	 * @return string
	 */
	private function get_count( array $parts ) {
		return 'SELECT ( ' . implode( ' ) + ( ', $parts ) . ' )';
	}

	/**
	 * Is query valid
	 *
	 * @param mixed $query SQL query.
	 *
	 * @return bool
	 */
	private function is_query_valid( $query ) {
		return $query instanceof Query;
	}
}
