<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 * Copyright (c) 2019-2020 CodeIgniter Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2019-2020 CodeIgniter Foundation
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 4.0.0
 * @filesource
 */

namespace CodeIgniter\Validation;

use Config\Database;

/**
 * Validation Rules.
 *
 * @package CodeIgniter\Validation
 */
class Rules
{
	//--------------------------------------------------------------------

	/**
	 * The value does not match another field in $data.
	 *
	 * @param string $str
	 * @param string $field
	 * @param array  $data  Other field/value pairs
	 *
	 * @return boolean
	 */
	public function differs(string $str = null, string $field, array $data): bool
	{
		return array_key_exists($field, $data) && $str !== $data[$field];
	}

	//--------------------------------------------------------------------

	/**
	 * Equals the static value provided.
	 *
	 * @param string $str
	 * @param string $val
	 *
	 * @return boolean
	 */
	public function equals(string $str = null, string $val): bool
	{
		return $str === $val;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns true if $str is $val characters long.
	 * $val = "5" (one) | "5,8,12" (multiple values)
	 *
	 * @param string $str
	 * @param string $val
	 *
	 * @return boolean
	 */
	public function exact_length(string $str = null, string $val): bool
	{
		$val = explode(',', $val);
		foreach ($val as $tmp)
		{
			if (is_numeric($tmp) && (int)$tmp === mb_strlen($str))
			{
				return true;
			}
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Greater than
	 *
	 * @param string $str
	 * @param string $min
	 *
	 * @return boolean
	 */
	public function greater_than(string $str = null, string $min): bool
	{
		return is_numeric($str) && $str > $min;
	}

	//--------------------------------------------------------------------

	/**
	 * Equal to or Greater than
	 *
	 * @param string $str
	 * @param string $min
	 *
	 * @return boolean
	 */
	public function greater_than_equal_to(string $str = null, string $min): bool
	{
		return is_numeric($str) && $str >= $min;
	}

	//--------------------------------------------------------------------

	/**
	 * Checks the database to see if the given value exist.
	 * Can ignore records by field/value to filter (currently
	 * accept only one filter).
	 *
	 * Example:
	 *    is_not_unique[table.field,where_field,where_value]
	 *    is_not_unique[menu.id,active,1]
	 *
	 * @param string $str
	 * @param string $field
	 * @param array  $data
	 *
	 * @return boolean
	 */
	public function is_not_unique(string $str = null, string $field, array $data): bool
	{
		// Grab any data for exclusion of a single row.
		list($field, $where_field, $where_value) = array_pad(explode(',', $field), 3, null);

		// Break the table and field apart
		sscanf($field, '%[^.].%[^.]', $table, $field);

		$db = Database::connect($data['DBGroup'] ?? null);

		$row = $db->table($table)
				  ->select('1')
				  ->where($field, $str)
				  ->limit(1);

		if (! empty($where_field) && ! empty($where_value))
		{
			if (! preg_match('/^\{(\w+)\}$/', $where_value))
			{
				$row = $row->where($where_field, $where_value);
			}
		}

		return (bool) ($row->get()
						->getRow() !== null);
	}

	//--------------------------------------------------------------------

	/**
	 * Value should be within an array of values
	 *
	 * @param string $value
	 * @param string $list
	 *
	 * @return boolean
	 */
	public function in_list(string $value = null, string $list): bool
	{
		$list = explode(',', $list);
		$list = array_map(function ($value) {
			return trim($value);
		}, $list);
		return in_array($value, $list, true);
	}

	//--------------------------------------------------------------------

	/**
	 * Checks the database to see if the given value is unique. Can
	 * ignore a single record by field/value to make it useful during
	 * record updates.
	 *
	 * Example:
	 *    is_unique[table.field,ignore_field,ignore_value]
	 *    is_unique[users.email,id,5]
	 *
	 * @param string $str
	 * @param string $field
	 * @param array  $data
	 *
	 * @return boolean
	 */
	public function is_unique(string $str = null, string $field, array $data): bool
	{
		// Grab any data for exclusion of a single row.
		list($field, $ignoreField, $ignoreValue) = array_pad(explode(',', $field), 3, null);

		// Break the table and field apart
		sscanf($field, '%[^.].%[^.]', $table, $field);

		$db = Database::connect($data['DBGroup'] ?? null);

		$row = $db->table($table)
				  ->select('1')
				  ->where($field, $str)
				  ->limit(1);

		if (! empty($ignoreField) && ! empty($ignoreValue))
		{
			if (! preg_match('/^\{(\w+)\}$/', $ignoreValue))
			{
				$row = $row->where("{$ignoreField} !=", $ignoreValue);
			}
		}

		return (bool) ($row->get()
						->getRow() === null);
	}

	//--------------------------------------------------------------------

	/**
	 * Less than
	 *
	 * @param string $str
	 * @param string $max
	 *
	 * @return boolean
	 */
	public function less_than(string $str = null, string $max): bool
	{
		return is_numeric($str) && $str < $max;
	}

	//--------------------------------------------------------------------

	/**
	 * Equal to or Less than
	 *
	 * @param string $str
	 * @param string $max
	 *
	 * @return boolean
	 */
	public function less_than_equal_to(string $str = null, string $max): bool
	{
		return is_numeric($str) && $str <= $max;
	}

	//--------------------------------------------------------------------

	/**
	 * Matches the value of another field in $data.
	 *
	 * @param string $str
	 * @param string $field
	 * @param array  $data  Other field/value pairs
	 *
	 * @return boolean
	 */
	public function matches(string $str = null, string $field, array $data): bool
	{
		return array_key_exists($field, $data) && $str === $data[$field];
	}

	//--------------------------------------------------------------------

	/**
	 * Returns true if $str is $val or fewer characters in length.
	 *
	 * @param string $str
	 * @param string $val
	 *
	 * @return boolean
	 */
	public function max_length(string $str = null, string $val): bool
	{
		return ($val >= mb_strlen($str));
	}

	//--------------------------------------------------------------------

	/**
	 * Returns true if $str is at least $val length.
	 *
	 * @param string $str
	 * @param string $val
	 *
	 * @return boolean
	 */
	public function min_length(string $str = null, string $val): bool
	{
		return ($val <= mb_strlen($str));
	}

	//--------------------------------------------------------------------

	/**
	 * Does not equal the static value provided.
	 *
	 * @param string $str
	 * @param string $val
	 *
	 * @return boolean
	 */
	public function not_equals(string $str = null, string $val): bool
	{
		return $str !== $val;
	}

	//--------------------------------------------------------------------

	/**
	 * Required
	 *
	 * @param mixed $str Value
	 *
	 * @return boolean          True if valid, false if not
	 */
	public function required($str = null): bool
	{
		if (is_object($str))
		{
			return true;
		}

		return is_array($str) ? ! empty($str) : (trim($str) !== '');
	}

	//--------------------------------------------------------------------

	/**
	 * The field is required when any of the other required fields are present
	 * in the data.
	 *
	 * Example (field is required when the password field is present):
	 *
	 *     required_with[password]
	 *
	 * @param $str
	 * @param string $fields List of fields that we should check if present
	 * @param array  $data   Complete list of fields from the form
	 *
	 * @return boolean
	 */
	public function required_with($str = null, string $fields, array $data): bool
	{
		$fields = explode(',', $fields);

		// If the field is present we can safely assume that
		// the field is here, no matter whether the corresponding
		// search field is present or not.
		$present = $this->required($str ?? '');

		if ($present)
		{
			return true;
		}

		// Still here? Then we fail this test if
		// any of the fields are present in $data
		// as $fields is the lis
		$requiredFields = [];

		foreach ($fields as $field)
		{
			if (array_key_exists($field, $data))
			{
				$requiredFields[] = $field;
			}
		}

		// Remove any keys with empty values since, that means they
		// weren't truly there, as far as this is concerned.
		$requiredFields = array_filter($requiredFields, function ($item) use ($data) {
			return ! empty($data[$item]);
		});

		return empty($requiredFields);
	}

	//--------------------------------------------------------------------

	/**
	 * The field is required when all of the other fields are present
	 * in the data but not required.
	 *
	 * Example (field is required when the id or email field is missing):
	 *
	 *     required_without[id,email]
	 *
	 * @param $str
	 * @param string $fields
	 * @param array  $data
	 *
	 * @return boolean
	 */
	public function required_without($str = null, string $fields, array $data): bool
	{
		$fields = explode(',', $fields);

		// If the field is present we can safely assume that
		// the field is here, no matter whether the corresponding
		// search field is present or not.
		$present = $this->required($str ?? '');

		if ($present)
		{
			return true;
		}

		// Still here? Then we fail this test if
		// any of the fields are not present in $data
		foreach ($fields as $field)
		{
			if (! array_key_exists($field, $data))
			{
				return false;
			}
		}

		return true;
	}

	//--------------------------------------------------------------------
}
