<?php
/**
 * Environment Checker
 *
 * Checks to see if the environment matches the passed conditions.
 * Supported conditions include:
 *
 * - Type of license (personal, plus, professional, ultimate).
 * - Plugin version: "this version onwards" or a range (">=2.34.0", "<=2.39.0").
 *
 * @package    AffiliateWP
 * @subpackage Admin\Utils
 * @copyright  Copyright (c) 2022, Sandhills Development, LLC
 * @license    GPL2+
 * @since      2.9.5
 */

namespace AffWP\Utils;

use AffWP\Core\License\License_Data;

#[\AllowDynamicProperties]

/**
 * Environment checker class.
 *
 * Checks environment against given license status and/or version.
 *
 * @since 2.9.5
 */
class Environment_Checker {

	/**
	 * @var License_Data
	 */
	protected $license_data;

	/**
	 * Types of license conditions that we support.
	 *
	 * @since 2.9.5
	 *
	 * @var string[]
	 */
	protected $valid_license_conditions = array(
		'personal',
		'plus',
		'professional',
		'ultimate',
	);

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->license_data = new License_Data();
	}

	/**
	 * Checks to see if this environment is valid given the specified conditions.
	 *
	 * @since 2.9.5
	 *
	 * @param array $conditions Conditions to check.
	 *
	 * @return bool
	 */
	public function is_valid( $conditions ) {
		// No conditions, always show.
		if ( empty( $conditions ) ) {
			return true;
		}

		// License conditions (if any) must match the site's license type.
		$license_conditions = $this->get_license_conditions( $conditions );
		if ( ! empty( $license_conditions ) && false === $this->license_type_matches( $license_conditions ) ) {
			return false;
		}

		// Version conditions (if any) must all be satisfied by the installed version.
		$version_conditions = $this->get_version_conditions( $conditions );
		if ( ! empty( $version_conditions ) && false === $this->version_matches( $version_conditions ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns only license-type conditions.
	 *
	 * @since 2.9.5
	 *
	 * @param array $conditions Conditions to check.
	 *
	 * @return array
	 */
	private function get_license_conditions( $conditions ) {
		return array_filter(
			$conditions,
			function( $condition ) {
				return $this->is_license_type( $condition );
			}
		);
	}

	/**
	 * Determines if a condition is a license type.
	 *
	 * @since 2.9.5
	 *
	 * @param string $condition Condition to check.
	 * @return bool
	 */
	private function is_license_type( $condition ) {
		return false !== array_search(
			$condition,
			$this->valid_license_conditions,
			true
		);
	}

	/**
	 * Determines if the current license type matches one of the license conditions.
	 *
	 * @since 2.9.5
	 *
	 * @param string $license_type License type that we're checking the site for.
	 *
	 * @return bool
	 */
	public function license_type_matches( $license_conditions ) {
		// Get license ID.
		$license_id = $this->license_data->get_license_id();

		// No price ID is found, assume Personal.
		if ( null === $license_id ) {
			$license_type = 'personal';
		} else {
			$license_type = $this->license_data->get_license_type( $license_id );
		}

		return in_array( strtolower( $license_type ), $license_conditions, true );
	}

	/**
	 * Returns only the version-condition tokens from a conditions array.
	 *
	 * A version condition begins with a digit, optionally prefixed with a
	 * >= or <= operator: "2.34.0", ">=2.34.0", "<=2.39.0". WordPress-slugged
	 * values such as "2-34-0" are accepted (normalized when compared).
	 *
	 * @since 2.34.2
	 *
	 * @param array $conditions Conditions to check.
	 * @return array
	 */
	private function get_version_conditions( $conditions ) {
		return array_values(
			array_filter(
				$conditions,
				function ( $condition ) {
					return $this->is_version_condition( $condition );
				}
			)
		);
	}

	/**
	 * Determines whether a condition is a version constraint.
	 *
	 * @since 2.34.2
	 *
	 * @param string $condition Condition to check.
	 * @return bool
	 */
	private function is_version_condition( $condition ) {
		if ( ! is_string( $condition ) ) {
			return false;
		}

		$value = preg_replace( '/^(>=|<=)/', '', trim( $condition ) );

		return (bool) preg_match( '/^\d+([.-]\d+)*$/', $value );
	}

	/**
	 * Determines whether the installed version satisfies every version condition.
	 *
	 * A bare or ">=" token requires the installed version to be greater than or
	 * equal to the target ("this version onwards"); a "<=" token requires it to
	 * be less than or equal. Multiple tokens are ANDed, so a band is expressed
	 * as ">=2.34.0" together with "<=2.39.0".
	 *
	 * @since 2.34.2
	 *
	 * @param array $version_conditions Version tokens to check.
	 * @return bool
	 */
	private function version_matches( $version_conditions ) {
		$installed = defined( 'AFFILIATEWP_VERSION' ) ? AFFILIATEWP_VERSION : '0';

		foreach ( $version_conditions as $condition ) {
			$condition = trim( $condition );

			if ( 0 === strpos( $condition, '<=' ) ) {
				$operator = '<=';
				$target   = substr( $condition, 2 );
			} elseif ( 0 === strpos( $condition, '>=' ) ) {
				$operator = '>=';
				$target   = substr( $condition, 2 );
			} else {
				// A bare version means "this version or higher".
				$operator = '>=';
				$target   = $condition;
			}

			// Normalize a WordPress-slugged version ("2-34-0") to dotted form.
			$target = str_replace( '-', '.', trim( $target ) );

			if ( '' === $target || ! version_compare( $installed, $target, $operator ) ) {
				return false;
			}
		}

		return true;
	}

}
