<?php
/**
 * ValidationRule.
 *
 * Provides the Model validation logic.
 *
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         2.2.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\Validation;

use InvalidArgumentException;

/**
 * ValidationRule object. Represents a validation method, error message and
 * rules for applying such method to a field.
 */
class ValidationRule
{
    /**
     * The method to be called for a given scope
     *
     * @var string|callable
     */
    protected $_rule;

    /**
     * The 'on' key
     *
     * @var string
     */
    protected $_on;

    /**
     * The 'last' key
     *
     * @var bool
     */
    protected $_last = false;

    /**
     * The 'message' key
     *
     * @var string
     */
    protected $_message;

    /**
     * Key under which the object or class where the method to be used for
     * validation will be found
     *
     * @var string
     */
    protected $_provider = 'default';

    /**
     * Extra arguments to be passed to the validation method
     *
     * @var array
     */
    protected $_pass = [];

    /**
     * Constructor
     *
     * @param array $validator [optional] The validator properties
     */
    public function __construct(array $validator = [])
    {
        $this->_addValidatorProps($validator);
    }

    /**
     * Returns whether this rule should break validation process for associated field
     * after it fails
     *
     * @return bool
     */
    public function isLast()
    {
        return (bool)$this->_last;
    }

    /**
     * Dispatches the validation rule to the given validator method and returns
     * a boolean indicating whether the rule passed or not. If a string is returned
     * it is assumed that the rule failed and the error message was given as a result.
     *
     * @param mixed $value The data to validate
     * @param array $providers associative array with objects or class names that will
     * be passed as the last argument for the validation method
     * @param array $context A key value list of data that could be used as context
     * during validation. Recognized keys are:
     * - newRecord: (boolean) whether or not the data to be validated belongs to a
     *   new record
     * - data: The full data that was passed to the validation process
     * - field: The name of the field that is being processed
     * @return bool|string|array
     * @throws \InvalidArgumentException when the supplied rule is not a valid
     * callable for the configured scope
     */
    public function process($value, array $providers, array $context = [])
    {
        $context += ['data' => [], 'newRecord' => true, 'providers' => $providers];

        if ($this->_skip($context)) {
            return true;
        }

        if (!is_string($this->_rule) && is_callable($this->_rule)) {
            $callable = $this->_rule;
            $isCallable = true;
        } else {
            $provider = $providers[$this->_provider];
            $callable = [$provider, $this->_rule];
            $isCallable = is_callable($callable);
        }

        if (!$isCallable) {
            $message = 'Unable to call method "%s" in "%s" provider for field "%s"';
            throw new InvalidArgumentException(
                sprintf($message, $this->_rule, $this->_provider, $context['field'])
            );
        }

        if ($this->_pass) {
            $args = array_values(array_merge([$value], $this->_pass, [$context]));
            $result = $callable(...$args);
        } else {
            $result = $callable($value, $context);
        }

        if ($result === false) {
            return $this->_message ?: false;
        }

        return $result;
    }

    /**
     * Checks if the validation rule should be skipped
     *
     * @param array $context A key value list of data that could be used as context
     * during validation. Recognized keys are:
     * - newRecord: (boolean) whether or not the data to be validated belongs to a
     *   new record
     * - data: The full data that was passed to the validation process
     * - providers associative array with objects or class names that will
     *   be passed as the last argument for the validation method
     * @return bool True if the ValidationRule should be skipped
     */
    protected function _skip($context)
    {
        if (!is_string($this->_on) && is_callable($this->_on)) {
            $function = $this->_on;

            return !$function($context);
        }

        $newRecord = $context['newRecord'];
        if (!empty($this->_on)) {
            if (($this->_on === 'create' && !$newRecord) || ($this->_on === 'update' && $newRecord)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Sets the rule properties from the rule entry in validate
     *
     * @param array $validator [optional]
     * @return void
     */
    protected function _addValidatorProps($validator = [])
    {
        foreach ($validator as $key => $value) {
            if (!isset($value) || empty($value)) {
                continue;
            }
            if ($key === 'rule' && is_array($value) && !is_callable($value)) {
                $this->_pass = array_slice($value, 1);
                $value = array_shift($value);
            }
            if (in_array($key, ['rule', 'on', 'message', 'last', 'provider', 'pass'], true)) {
                $this->{"_$key"} = $value;
            }
        }
    }

    /**
     * Returns the value of a property by name
     *
     * @param string $property The name of the property to retrieve.
     * @return mixed
     */
    public function get($property)
    {
        $property = '_' . $property;
        if (isset($this->{$property})) {
            return $this->{$property};
        }
    }
}
