<?php

namespace Engine;

/**
 * The class is used for making simple validations on a certain value.
 * @author Gabriel Grosu
 */
class Validator {

	/**
	 * The value to be validated
	 * @var Mixed
	 */
	protected $value;

	/**
	 * The validations list
	 * @var array
	 */
	protected $validations = [];


	/**
	 * The class constructor.
	 * @param Mixed $value
	 * @param Array $validationsList
	 */
	public function __construct($value, array $validationsList=[])
	{
		$this->value=$value;
		$this->setValidations($validationsList);
	}


	/**
	 * Rewrite the value to be validated
	 * @param Mixed $value
	 * @return Boolean
	 */
	public function setValue($value)
	{
		$this->value=$value;
		return true;
	}


	/**
	 * Set new validations
	 * @param Array $validationsList
	 * @return Boolean
	 */
	public function setValidations(Array $validationsList)
	{
		$this->validations = [];
		// store the validations
		foreach($validationsList as $code=>$rule) {
			$this->addValidation($code, $rule);
		}
		return true;
	}


	/**
	 * Sets a validation (and saves it into the object).
	 * @param String $var
	 * @param Mixed $value
	 * @return Boolean
	 */
	public function addValidation($var, $value)
	{

		// Validating: type
		if ($var=="type") {
			// basic type
			if ($value=="digits" or $value=="int" or $value=="float" or $value=="alpha" or $value=="alnum" or $value=="date" or $value=="time" or  $value=="datetime" or $value=="email" or $value=="url" or $value=="string" ) {
				$this->validations['type']=$value;
				return true;
			}
			// type=username
			elseif ($value=='username') {
 				$this->validations['regex']="/^([^_%&\"\'\\\\])+$/"; // does not allow: _, %, &, ", ', \
				return true;
 			} else {
				throw new \Exception('The validator type can only be: digits, int, float, alpha, alnum, date, time, datetime, email, url or string.');
			}
		}

		// Validating:  minLength, maxLength, fixLength
		if ($var=='minLength' or $var=='maxLength' or $var=='fixLength') {
			if (is_numeric($value) and $value>=0) {
				$this->validations[$var]=$value;
				return true;
			} else {
				throw new \Exception('The validator can only get numeric (greater or equal to 0) values for validating `minLength`, `maxLength` and `fixLength`.');
			}
		}

		// Validating:  minLength, maxLength, fixLength, minValue, maxValue
		if ($var=='minValue' or $var=='maxValue') {
			$this->validations[$var]=$value;
			return true;
		}

		// Validating: lowercase, uppercase
		if ($var=='case') {
			if ($value=='upper' or $value=='lower') {
				$this->validations[$var]=$value;
				return true;
			} else {
				throw new \Exception('The validator can only get `upper` or `lower` for `case` validation.');
			}
		}

		// Validating: regex
		if ($var=='regex') {
			$this->validations[$var]='/'.$value.'/';
			return true;
		}

		// Validating: list
		if ($var=='list') {
			if (is_array($value)) {
				$this->validations['list']=$value;
				return true;
			} else {
				throw new \Exception("The validator of 'list' type must receive an array so the values are searched within.");
			}
		}

		// Validating: allowedValues
		if ($var=='allowedValues') {
			if (is_array($value) and count($value)) {
				$this->validations['allowedValues']=$value;
				return true;
			} else {
				throw new \Exception("The validator of 'allowedValues' type can receive an array of allowed strings (at least one value must be set).");
			}
		}

		throw new \Exception("The validator variable (`$var`) is not correct.");
	}


	/**
	 * Validates the collected data
	 * @return Boolean
	 */
	public function check()
	{

		if ($this->getProblems()) {
			return false;
		} else {
			return true;
		}

	}


	/**
	 * Get detailed problems returned by the validator
	 * @return Array
	 */
	public function getProblems()
	{

		$problems=[];

		// CHECK IF THE VALUE IS WITHIN THE allowedValues LIST (exits function if found) ----------
		if ( strlen($this->value)>0 and $this->validations['allowedValues'] and in_array($this->value, $this->validations['allowedValues'])) {
			return [];
		}

		// CHECK "type" ---------------------------------------------------------------------------
		if (
			( $this->validations['type']=='digits' and !$this->isDigits() ) or
			( $this->validations['type']=='int' and !$this->isInt() ) or
			( $this->validations['type']=='float' and !$this->isFloat() ) or
			( $this->validations['type']=='alpha' and !$this->isAlpha() ) or
			( $this->validations['type']=='alnum' and !$this->isAlnum() ) or
			( $this->validations['type']=='string' and !$this->isString() )
		) {
			$problems['type']=$this->validations['type'];
		}
		// check 'email'
		if ( $this->validations['type']=='email' and !$this->isEmail() ) {
			$problems['type']=$this->validations['type'];
		}
		// check 'url'
		if ( $this->validations['type']=='url' and !$this->isUrl() ) {
			$problems['type']=$this->validations['type'];
		}
		// check 'date'
		if ( $this->validations['type']=='date' and !$this->isDate() ) {
			$problems['type']=$this->validations['type'];
		}
		// check 'time'
		if ( $this->validations['type']=='time' and !$this->isTime() ) {
			$problems['type']=$this->validations['type'];
		}
		// check 'datetime'
		if ( $this->validations['type']=='datetime' and !$this->isDatetime() ) {
			$problems['type']=$this->validations['type'];
		}

		// CHECK 'list' ---------------------------------------------------------------------------
		if ( $this->validations['list'] and !$this->inList($this->validations['list']) ) {
			$problems['list']=$this->validations['list'];
		}

		// CHECK THE STRING LENGTH VALIDATIONS ----------------------------------------------------
		if ( isset($this->validations['minLength']) and !$this->minLength($this->validations['minLength']) ) {
			$problems['minLength']=$this->validations['minLength'];
		}
		if ( isset($this->validations['maxLength']) and !$this->maxLength($this->validations['maxLength']) ) {
			$problems['maxLength']=$this->validations['maxLength'];
		}
		if ( isset($this->validations['fixLength']) and !$this->fixLength($this->validations['fixLength']) ) {
			$problems['fixLength']=$this->validations['fixLength'];
		}

		// CHECK MIN/MAX VALUE (works with numeric, string, date, datetime - checks depending on type)
		if ( isset($this->validations['minValue']) and !$this->minValue($this->validations['minValue']) ) {
			if ($this->validations['type']=='date') $problems['minDate']=$this->validations['minValue'];
			elseif ($this->validations['type']=='datetime') $problems['minDatetime']=$this->validations['minValue'];
			else $problems['minValue']=$this->validations['minValue'];
		}
		if ( isset($this->validations['maxValue']) and !$this->maxValue($this->validations['maxValue']) ) {
			if ($this->validations['type']=='date') $problems['maxDate']=$this->validations['maxValue'];
			elseif ($this->validations['type']=='datetime') $problems['maxDatetime']=$this->validations['maxValue'];
			else $problems['maxValue']=$this->validations['maxValue'];
		}

		// CHECK lowercase / uppercase ------------------------------------------------------------
		if ( $this->validations['case']=='lower' and !$this->isLower() ) {
			$problems['case']='lower';
		}
		if ( $this->validations['case']=='upper' and !$this->isUpper() ) {
			$problems['case']='upper';
		}

		// CHECK regex ----------------------------------------------------------------------------
		if ( $this->validations['regex'] and !$this->isRegex($this->validations['regex']) ) {
			$problems['regex']=$this->validations['regex'];
		}

		// set the problems (to be cached)
		return $problems;

	}


	/**
	 * Returns an array of strings containing the messages with the problems
	 * @return Array
	 */
	public function getTranslatedProblems()
	{

		// get the list of problems
		$problems=$this->getProblems();

		// if no problems collected - returns an empty array
		if (!$problems) return [];

		$_LANG = \App\Loader_Lang::getInstance()->getGlobal();

		// collect messages
		$messages=[];
		foreach($problems as $code=>$limitation) {

			$msgKey = $code . '|' . (is_array($limitation) ? reset($limitation) : $limitation);

			// code = type
			if ($code=='type') {
				$messages[$msgKey]=$_LANG['Validator']['messages']['type'][$limitation];
			}
			// code = list
			elseif ($code=='list') {
				$possibVals=implode(', ', $limitation);
				$messages[$msgKey]=sprintf($_LANG['Validator']['messages']['list'], $possibVals);
			}
			// code = minLength / maxLength, fixLength
			elseif ($code=='minLength' or $code=='maxLength' or $code=='fixLength' or $code=='minValue' or $code=='maxValue') {
				$messages[$msgKey]=sprintf($_LANG['Validator']['messages'][$code], $limitation);
			}
			// code = minDate / maxDate
			elseif ($code=='minDate' or $code=='maxDate') {
				$messages[$msgKey]=sprintf($_LANG['Validator']['messages'][$code], \Engine\DateTime::isoToStd($limitation));
			}
			// code = minDatetime / maxDatetime
			elseif ($code=='minDatetime' or $code=='maxDatetime') {
				$messages[$msgKey]=sprintf($_LANG['Validator']['messages'][$code], $limitation);
			}
			// code = upper / lower
			elseif ($code=='case') {
				if ($limitation=='lower') $messages[$msgKey]=$_LANG['Validator']['messages']['lowercase'];
				if ($limitation=='upper') $messages[$msgKey]=$_LANG['Validator']['messages']['uppercase'];
			}
			// code = regex
			elseif ($code=='regex') {
				$messages[$msgKey]=$_LANG['Validator']['messages']['regex'];
			}
			// invalid code
			else {
				throw new \Exception("The validator is not a standard one (`$code`).");
			}

		}

		return $messages;

	}


	/**
	 * Static method for collecting validating data
	 * @param Mixed $value
	 * @param Array $validationsList
	 * @return Validator
	 */
	static public function set($value, array $validationsList=[])
	{
		return new self($value, $validationsList);
	}


	/**
	 *Checks that the validating value contains only digits.
	 * @return Boolean
	 */
	public function isDigits()
	{
		return ctype_digit($this->value);
	}


	/**
	 * Checks that the validating value is int.
	 * @return Boolean
	 */
	public function isInt()
	{
		return (is_numeric($this->value) and (Int)$this->value==$this->value);
	}


	/**
	 *Checks that the validating value is float.
	 * @return Boolean
	 */
	public function isFloat()
	{
		return is_numeric($this->value);
	}


	/**
	 * Checks that the validating value is alphanumeric.
	 * @return Boolean
	 */
	public function isAlnum()
	{
		return ctype_alnum($this->value);
	}

	/**
	 * Checks that the validating value contains only letters.
	 * @return Boolean
	 */
	public function isAlpha()
	{
		return ctype_alpha($this->value);
	}


	/**
	 * Checks that the validating value is a string.
	 * @return Boolean
	 */
	public function isString()
	{
		return is_string($this->value);
	}


	/**
	 * Checks that the validating value is an email.
	 * @return Boolean
	 */
	public function isEmail()
	{
		return (Bool)filter_var($this->value, FILTER_VALIDATE_EMAIL);
	}


	/**
	 * Checks that the validating value is an URL.
	 * @return Boolean
	 */
	public function isUrl()
	{
		/*
		 *
		 * @see Good to know: https://d-mueller.de/blog/why-url-validation-with-filter_var-might-not-be-a-good-idea/		 *
		 * return (Bool)filter_var($this->value, FILTER_VALIDATE_URL);
		 */

		$xtr = new \App\Utils\BaseParser\Utils\Extractor\Single();


		// define reusable regexp components
		$port = '(:[0-9]+)?';				// max port number should be 65535 but its not validated here anyway
		$location = '(\/[^<]*)?';			// location format (Ex: index.php?action=index)
		$protocol = '(http|https):\/\/';	// protocol

		/*
		 * check the domain format
		 * Ex:
		 * 	www.google.ro
		 * 	google.ro
		 * 	google.ro.com
		 */
		$letterPattern = "/^{$protocol}(www\.)?(([0-9\w\-_]+\.)+)?([a-z0-9][\w\-_]*)\.([a-z][\w]{1,10}){$port}{$location}$/i";

		// ipv4 validation
		$ipV4Pattern = "/^{$protocol}(\d{1,3})(\.\d{1,3}){3}{$port}{$location}$/i";

		// ipv6 validation
		$ipV6Pattern = "/^{$protocol}([0-9a-f]{1,4})(\:[0-9a-f]{1,4}){1,7}?{$port}{$location}$/i";

		return 	(Bool) ($xtr->contains($ipV4Pattern, $this->value) || $xtr->contains($ipV6Pattern, $this->value) || $xtr->contains($letterPattern, $this->value));
	}


	/**
	 * Checks that the validating value is a date.
	 * @return Boolean
	 */
	public function isDate()
	{
		return validDate($this->value);
	}

	/**
	 * Check if the validating value is a time format.
	 * @return Boolean
	 */
	public function isTime()
	{
		// if value not set
		if (!$this->value) return false;

		// get time components
		list($h, $i, $s) = explode(':', $this->value);
		// check hour
		if (!is_numeric($h) or $h<0 or $h>23) return false;
		// check minutes
		if (!is_numeric($i) or $i<0 or $i>59) return false;
		// check seconds
		if ($s and (!is_numeric($i) or $s<0 or $s>59)) return false;
		// if this point reached - H:i:s is okay
		return true;
	}

	/**
	 * Checks that the validating value is a datetime (Y-m-d H:i:s).
	 * @return Boolean
	 */
	public function isDatetime()
	{
		list($date, $time)=explode(' ', $this->value);
		// check date
		if (!validDate($date)) return false;
		// check time
		if ($time) {
			list($h, $i, $s)=explode(':', $time);
			if (!is_numeric($h) or $h<0 or $h>23) return false;
			if (!is_numeric($i) or $i<0 or $i>59) return false;
			if ($s and (!is_numeric($s) or $s<0 or $s>59)) return false;
		}
		return true;
	}


	/**
	 * Checks that the validating value is found inside a list.
	 * @return Boolean
	 */
	public function inList($list)
	{
		return in_array($this->value, $list);
	}


	/**
	 * Checks that the length of the validating value is greater than some specified length.
	 * @return Boolean
	 */
	public function minLength($length)
	{
		if ($length<0) throw new \Exception("It doesn't make sense to test that the length of a string is greater than a negative value.");
		return ( mb_strlen($this->value)>=$length);
	}


	/**
	 * Checks that the length of the validating value is smaller than some specified length.
	 * @return Boolean
	 */
	public function maxLength($length)
	{
		if ($length<0) throw new \Exception("It doesn't make sense to test that the length of a string is smaller than a negative value.");
		return ( mb_strlen($this->value)<=$length);
	}


	/**
	 * Checks that the length of the validating value is exactly the same as some specified length.
	 * @return Boolean
	 */
	public function fixLength($length)
	{
		if ($length<0) throw new \Exception("It doesn't make sense to test that the length of a string is exactly than a negative value.");
		return ( mb_strlen($this->value)==$length);
	}


	/**
	 * Checks that the validating value is greater than some specified value.
	 * @return Boolean
	 */
	public function minValue($value)
	{
		return ($this->value>=$value);
	}


	/**
	 * Checks that the validating value is smaller than some specified value.
	 * @return Boolean
	 */
	public function maxValue($value)
	{
		return ($this->value<=$value);
	}


	/**
	 * Checks that the validating value is written in lowercase.
	 * @return Boolean
	 */
	public function isLower()
	{
		$uncleanedValue=htmlUnclean($this->value);
		return (mb_strtolower($uncleanedValue)==$uncleanedValue);
	}


	/**
	 * Checks that the validating value is written in uppercase.
	 * @return Boolean
	 */
	public function isUpper()
	{
		$uncleanedValue=htmlUnclean($this->value);
		return (mb_strtoupper($uncleanedValue)==$uncleanedValue);
	}


	/**
	 * Checks that the validating value respects a certain regular expression.
	 * @return Boolean
	 */
	public function isRegex($regex)
	{
		return preg_match($regex, $this->value);
	}

}


/*

// EXAMPLES ON HOW TO USE THE VALIDATOR CLASS /////////////////////////////////////////////////////

// all possible validators...
$obj1=new \Engine\Validator('100.1', array('type'=>'digits'));
$obj2=new \Engine\Validator('100a', array('type'=>'float'));
$obj3=new \Engine\Validator('100-a', array('type'=>'alnum'));
$obj4=new \Engine\Validator('50', array('list'=>array(0, 10, 50, 30, 80)));
$obj5=new \Engine\Validator('test@test.ro', array('type'=>'email'));
$obj6=new \Engine\Validator('http://192.168.1.1', array('type'=>'url'));
$obj7=new \Engine\Validator(array('fff'), array('type'=>'string'));
$obj8=new \Engine\Validator('test', array('type'=>'string', 'minLength'=>5));
$obj9=new \Engine\Validator('test', array('type'=>'string', 'maxLength'=>3));
$obj10=new \Engine\Validator('test', array('type'=>'string', 'fixLength'=>3));
$obj11=new \Engine\Validator('124', array('type'=>'int', 'minValue'=>100));
$obj12=new \Engine\Validator('124', array('type'=>'int', 'maxValue'=>23));
$obj13=new \Engine\Validator('ABCDEFGH', array('case'=>'upper', 'maxLength'=>23));
$obj14=new \Engine\Validator('124', array('case'=>'upper', 'maxLength'=>23));
$obj15=new \Engine\Validator('124', array('regex'=>'/^([0-9])+$/'));
$obj16=new \Engine\Validator('124', array('type'=>'date', 'minValue'=>'1900-01-01', 'maxValue'=>'2000-01-01'));

$obj1->getProblems(); // returns an internal Array of problems (if any); empty array otherwise
$obj1->getTranslatedProblems(); // returns an internal Array of _LANG translations for problems (if any); empty array otherwise
$obj1->check(); // returns true/false to indicate if the value is valid


// adding multiple validations
$obj1=new \Engine\Validator('address@email.com');
$obj1->type='email'; // add 'email' validation
$obj1->minLength=20; // also adds a minLength validation
$obj1->getTranslatedProblems();

// adding and eliminating validations
$obj1=new \Engine\Validator(100);
$obj1->type='float';
$obj1->minValue=200; // value is not  greater or equal to 200
$obj1->list=array(10, 20, 30, 50, 80, 100, 150); // but it is in this list
unset($obj1->minValue); // we eliminate the "greater or equal" conditions
$obj1->getTranslatedProblems(); // value is valid



// rewriting the value or the validations that are tested
$obj1=new \Engine\Validator(100);
$obj1->type='float';
$obj1->minValue=50;
$obj1->check(); // returns true (100>=50)
$obj1->setValue(30);
$obj1->check(); // returns false (30<50)
$obj1->setValidations(array('type'=>'int', 'list'=>array(10, 20, 30)));
$obj1->check(); // returns true (30 is in the above list)


// static (simple) validation
\Engine\Validator::set(10.02, array('type'=>'float'))->check(); // returns true

// static (advanced) validation
$obj=\Engine\Validator::set(10.02);
$obj->setValidations(array('type'=>'float', 'minLength'=>2, 'minValue'=>0));
$obj->getProblems(); // returns empty array (all validations are ok)


*/
