<?php
/**
 * @package   SureCartCore
 * @author    SureCart <support@surecart.com>
 * @copyright 2017-2019 SureCart
 * @license   https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0
 * @link      https://surecart.com/
 */

namespace SureCartCore\Routing\Conditions;

use Closure;
use ReflectionClass;
use ReflectionException;
use SureCartCore\Exceptions\ConfigurationException;

/**
 * Check against the current url
 */
class ConditionFactory {
	const NEGATE_CONDITION_PREFIX = '!';

	/**
	 * Registered condition types.
	 *
	 * @var array<string, string>
	 */
	protected $condition_types = [];

	/**
	 * Constructor.
	 *
	 * @codeCoverageIgnore
	 * @param array<string, string> $condition_types
	 */
	public function __construct( $condition_types ) {
		$this->condition_types = $condition_types;
	}

	/**
	 * Create a new condition.
	 *
	 * @param  string|array|Closure $options
	 * @return ConditionInterface
	 */
	public function make( $options ) {
		if ( is_string( $options ) ) {
			return $this->makeFromUrl( $options );
		}

		if ( is_array( $options ) ) {
			return $this->makeFromArray( $options );
		}

		if ( $options instanceof Closure ) {
			return $this->makeFromClosure( $options );
		}

		throw new ConfigurationException( 'Invalid condition options supplied.' );
	}

	/**
	 * Ensure value is a condition.
	 *
	 * @param  string|array|Closure|ConditionInterface $value
	 * @return ConditionInterface
	 */
	public function condition( $value ) {
		if ( $value instanceof ConditionInterface ) {
			return $value;
		}

		return $this->make( $value );
	}

	/**
	 * Get condition class for condition type.
	 *
	 * @param  string $condition_type
	 * @return string|null
	 */
	protected function getConditionTypeClass( $condition_type ) {
		if ( ! isset( $this->condition_types[ $condition_type ] ) ) {
			return null;
		}

		return $this->condition_types[ $condition_type ];
	}

	/**
	 * Check if the passed argument is a registered condition type.
	 *
	 * @param  mixed $condition_type
	 * @return boolean
	 */
	protected function conditionTypeRegistered( $condition_type ) {
		if ( ! is_string( $condition_type ) ) {
			return false;
		}

		return $this->getConditionTypeClass( $condition_type ) !== null;
	}

	/**
	 * Check if a condition is negated.
	 *
	 * @param  mixed $condition
	 * @return boolean
	 */
	protected function isNegatedCondition( $condition ) {
		return (
			is_string( $condition )
			&&
			strpos( $condition, static::NEGATE_CONDITION_PREFIX ) === 0
		);
	}

	/**
	 * Parse a negated condition and its arguments.
	 *
	 * @param  string $type
	 * @param  array  $arguments
	 * @return array
	 */
	protected function parseNegatedCondition( $type, $arguments ) {
		$negated_type = substr( $type, strlen( static::NEGATE_CONDITION_PREFIX ) );
		$arguments    = array_merge( [ $negated_type ], $arguments );

		$type      = 'negate';
		$condition = call_user_func( [ $this, 'make' ], $arguments );

		return [
			'type'      => $type,
			'arguments' => [ $condition ],
		];
	}

	/**
	 * Parse the condition type and its arguments from an options array.
	 *
	 * @param  array $options
	 * @return array
	 */
	protected function parseConditionOptions( $options ) {
		$type      = $options[0];
		$arguments = array_values( array_slice( $options, 1 ) );

		if ( $this->isNegatedCondition( $type ) ) {
			return $this->parseNegatedCondition( $type, $arguments );
		}

		if ( ! $this->conditionTypeRegistered( $type ) ) {
			if ( is_callable( $type ) ) {
				return [
					'type'      => 'custom',
					'arguments' => $options,
				];
			}

			throw new ConfigurationException( 'Unknown condition type specified: ' . $type );
		}

		return [
			'type'      => $type,
			'arguments' => $arguments,
		];
	}

	/**
	 * Create a new condition from a url.
	 *
	 * @param  string $url
	 * @return ConditionInterface
	 */
	protected function makeFromUrl( $url ) {
		return new UrlCondition( $url );
	}

	/**
	 * Create a new condition from an array.
	 *
	 * @param  array $options
	 * @return ConditionInterface
	 */
	protected function makeFromArray( $options ) {
		if ( count( $options ) === 0 ) {
			throw new ConfigurationException( 'No condition type specified.' );
		}

		if ( is_array( $options[0] ) ) {
			return $this->makeFromArrayOfConditions( $options );
		}

		$condition_options = $this->parseConditionOptions( $options );
		$condition_class   = $this->getConditionTypeClass( $condition_options['type'] );

		try {
			$reflection = new ReflectionClass( $condition_class );
			/** @var ConditionInterface $instance */
			$instance = $reflection->newInstanceArgs( $condition_options['arguments'] );
			return $instance;
		} catch ( ReflectionException $e ) {
			throw new ConfigurationException( 'Condition class "' . $condition_class . '" does not exist.' );
		}
	}

	/**
	 * Create a new condition from an array of conditions.
	 *
	 * @param  array $options
	 * @return ConditionInterface
	 */
	protected function makeFromArrayOfConditions( $options ) {
		$conditions = array_map(
			function ( $condition ) {
				if ( $condition instanceof ConditionInterface ) {
					  return $condition;
				}
				return $this->make( $condition );
			},
			$options
		);

		return new MultipleCondition( $conditions );
	}

	/**
	 * Create a new condition from a closure.
	 *
	 * @param  Closure $closure
	 * @return ConditionInterface
	 */
	protected function makeFromClosure( Closure $closure ) {
		return new CustomCondition( $closure );
	}

	/**
	 * Merge group condition attribute.
	 *
	 * @param  string|array|Closure|ConditionInterface|null $old
	 * @param  string|array|Closure|ConditionInterface|null $new
	 * @return ConditionInterface|null
	 */
	public function merge( $old, $new ) {
		if ( empty( $old ) ) {
			if ( empty( $new ) ) {
				return null;
			}

			return $this->condition( $new );
		} elseif ( empty( $new ) ) {
			return $this->condition( $old );
		}

		return $this->mergeConditions( $this->condition( $old ), $this->condition( $new ) );
	}

	/**
	 * Merge condition instances.
	 *
	 * @param  ConditionInterface $old
	 * @param  ConditionInterface $new
	 * @return ConditionInterface
	 */
	public function mergeConditions( ConditionInterface $old, ConditionInterface $new ) {
		if ( $old instanceof UrlCondition && $new instanceof UrlCondition ) {
			return $old->concatenate( $new->getUrl(), $new->getUrlWhere() );
		}

		return $this->makeFromArrayOfConditions( [ $old, $new ] );
	}
}
