<?php

namespace SWAK\Utils\Validator;


use SWAK\Exceptions\OutOfRangeException;
use SWAK\Exceptions\InvalidArgumentException;


/**
 * Validator class
 * known validators so far:
 *  - validateDatetime
 *  - validateDate
 *  - validateTime
 *  - validateInteger
 *  - validateString
 *  - validateText
 *  - validateRequired
 *  - validateEmail
 *  - validateUrl
 *
 */
class Validator {



    /**
     * Provide easy access to validation for classes implementing the
     * ValidatoInterface.
     * Error messages are attached to the object using their addErrors()
     * method.
     *
     * @param ValidatorInterface
     * @return bool
     */
    public function validateObject(ValidatorInterface $object) {
        //var_dump($object);

        $errorMessages = $this->validate(
            $object->getValue(),
            $object->getValidationRules()
        );

        $object->addErrorMessages($errorMessages);

        return count($errorMessages) === 0;
    }
    


    /**
     * Validate a string against a list of validators.
     * The validators are methods in this class. They have to be
     * specified using an array in this way
     *
     *  $listOfValidators = array(
     *      $validator1,
     *      $validator2,
     *      ...
     *      $validatorN,
     *  );
     *
     * where
     * 
     *  $validatorN = array(
     *      'validator' => 'validateMethod',
     *      'options' => array(
     *          'min' => ...,
     *      )
     *  )
     *
     * ValidateMethod must start with "validate".
     * Unknown validators are ignored, unknown options also.
     * @todo throw Exception/write log info for invalid validators/options
     * 
     * @param string to be validated
     * @param array containing the validators
     * @return array errormessages returned from each validator
     */
    public function validate($toBeValidated, array $listOfValidators) {

        //
        // argument check
        //
        if(!is_string($toBeValidated) && !is_null($toBeValidated)) {
            throw new InvalidArgumentException('Invalid argument.');
        }



        //
        // initialize return value
        //
        $errorMessages = array();



        //
        // loop over the validators
        //
        foreach($listOfValidators as $validator) {

            //
            // ignore unknown validators and wrong options
            //
            if(isset($validator['validator'])
                && is_string($validator['validator'])
                && substr( $validator['validator'], 0, 8 ) == 'validate'
                && method_exists($this, $validator['validator'])
                ) {
                $options = isset($validator['options']) && is_array($validator['options'])
                            ? $validator['options']
                            : array()
                ;

                $method = $validator['validator'];
                $errorMessage = $this->$method($toBeValidated, $options);

                if($errorMessage !== null) {
                    $errorMessages[] = $errorMessage;
                }
            }
        }

        
        return $errorMessages;
    }



    /**
     * Generic Datetime validator. Add method specifying the $format
     * to customize (as validateDateTime).
     * Accepted are the strings in the 'Y-m-d G:i:s' format which
     * represent a valid datetime
     *
     * @param string to be validated
     * @param array addional constraints
     * @return string|null errormessage
     */
    protected function genericDatetime($format, $toBeValidated, array $options = array()) {

        //
        // initialize return value
        //
        $errorMessages = array();
        


        //
        // set constaints from options
        // known constraints are min, max
        // ignore other constraints
        //
        $constraints = array();

        if(array_key_exists('min', $options)) {
            $constraints['min'] = \DateTime::createFromFormat($format, $options['min']);

            $check =    $constraints['min'] !== false
                    &&  \DateTime::getLastErrors()["warning_count"] == 0
                    &&  \DateTime::getLastErrors()["error_count"] == 0;

            if(!$check) {
                throw new InvalidArgumentException('Invalid option.');
            }
        }

        if(array_key_exists('max', $options)) {
            $constraints['max'] = \DateTime::createFromFormat($format, $options['max']);

            $check =    $constraints['max'] !== false
                    &&  \DateTime::getLastErrors()["warning_count"] == 0
                    &&  \DateTime::getLastErrors()["error_count"] == 0;

            if(!$check) {
                throw new InvalidArgumentException('Invalid option.');
            }
        }



        if($toBeValidated !== null && is_string($toBeValidated) && strlen($toBeValidated) > 0) {

            //
            // check for correct syntax
            //
            $date = \DateTime::createFromFormat($format, $toBeValidated);

            $bool =     $date !== FALSE
                    &&  \DateTime::getLastErrors()["warning_count"] == 0 // don't accept 2012-02-34 27:00:00
                    &&  \DateTime::getLastErrors()["error_count"] == 0 // or 2012-10-10 26:00:98
            ;

            if($bool === false) {
                
                $errorMessages[] = 'Invalid DateTime or wrong format! Use "'.$format.'".';
                
            } else {

                //
                // check for constraints
                //
                if(isset($constraints['min']) && $constraints['min'] > $date) {
                    $errorMessages[] = 'Lower bound: ' . $options['min'] . '.';
                }


                if(isset($constraints['max']) && $constraints['max'] < $date) {
                    $errorMessages[] = 'Upper bound: ' . $options['max'] . '.';
                }
            }
        }

        return count($errorMessages) > 0 ? implode(' \n', $errorMessages) : null;
    }



    /**
     * Validate a datetime string in 'Y-m-d G:i:s' format.
     *
     * 
     * @param string to be validated
     * @param array addional constraints
     * @return string|null errormessage
     */
    protected function validateDateTime($toBeValidated, array $options = array()) {

        return $this->genericDatetime('Y-m-d G:i:s', $toBeValidated, $options);
    }



    /**
     * Validate a date string in 'Y-m-d' format.
     *
     * 
     * @param string to be validated
     * @param array addional constraints
     * @return string|null errormessage
     */
    protected function validateDate($toBeValidated, array $options = array()) {

        return $this->genericDatetime('Y-m-d', $toBeValidated, $options);
    }



    /**
     * Accepted are the strings in the 'HHH:MM:SS' format which
     * represent a valid time (from '-838:59:59' to
     * '-838:59:59')
     *
     * @param string
     * @param array with options, min, max
     * @return string|null errormessages
     */
    protected function validateTime($toBeValidated, array $options = array()) {

        //
        // initialize return value
        //
        $errorMessages = array();



        //
        // required format (-)HHH:MM:SS
        //
        $pattern = '/^(-)?([0-9]{1,3}):([0-5][0-9]):([0-5][0-9])$/';



        //
        // check options
        // save in other format as $min/$max so that the values can be compared
        //
        $min = null;
        $max = null;
        if(array_key_exists('min', $options)) {
            $matches = array();
            $validFormat = preg_match($pattern, $options['min'], $matches);
            
            if(!$validFormat) {
                throw new InvalidArgumentException('Invalid option, give as (-)HHH:MM:SS.');
            }
            
            $min = (int) ($matches[1] . $matches[2] . $matches[3]. $matches[4]);
        }

        if(array_key_exists('max', $options)) {
            $matches = array();
            $validFormat = preg_match($pattern, $options['max'], $matches);

            if(!$validFormat) {
                throw new InvalidArgumentException('Invalid option, give as (-)HHH:MM:SS.');
            }
            
            $max = (int) ($matches[1] . $matches[2] . $matches[3]. $matches[4]);
        }



        //
        // only validate if value is set!
        //
        if($toBeValidated !== null && is_string($toBeValidated) && strlen($toBeValidated) > 0) {

            //
            // check for correct format
            //
            $matches = array();
            $validFormat = preg_match($pattern, $toBeValidated, $matches);
            
            if(!$validFormat) {
                $errorMessages[] = 'Invalid Time! Use "(-)HHH:MM:SS"';
            } else {

                //
                // optional constraints
                //
                $givenValue = (int) ($matches[1] . $matches[2] . $matches[3]. $matches[4]);

                if($min !== null && $min >= $givenValue) {
                    $errorMessages[] = 'Lower bound: ' . $options['min'] . '.';
                }

                if($max !== null && $max <= $givenValue) {
                    $errorMessages[] = 'Upper bound: ' . $options['max'] . '.';
                }
            }
        }


        return count($errorMessages) > 0 ? implode(' \n', $errorMessages) : null;
    }




    /**
     * Validate string to correspond to an integer. A range
     * may be specified using the options.
     * 
     * @param string
     * @param array with options, min, max
     * @return string|null errormessages
     */
    protected function validateInteger($toBeValidated, array $options = array()) {

        //
        // initialize return value
        //
        $errorMessages = array();



        //
        // check options
        //
        if(array_key_exists('min', $options)) {
            
            $valid = filter_var($options['min'], FILTER_VALIDATE_INT) !== FALSE;

            if(!$valid) {
                throw new InvalidArgumentException(
                    'Wrong lower limit. Maybe smaller than -PHP_INT_MAX on this system.
                ');
            }
        }

        if(array_key_exists('max', $options)) {
            
            $valid = filter_var($options['max'], FILTER_VALIDATE_INT) !== FALSE;

            if(!$valid) {
                throw new InvalidArgumentException(
                    'Wrong upper limit. Maybe bigger than PHP_INT_MAX on this system.
                ');
            }
        }



        // cut leading zeroes if the integer is given as string of digits
        // as filter_var("012", FILTER_VALIDATE_INT) would return zero
        if(is_string($toBeValidated)) {
            $toBeValidated = ltrim($toBeValidated, '0');
        }



        if($toBeValidated !== null && is_string($toBeValidated) && strlen($toBeValidated) > 0) {
            //
            // validate
            //
            $bool = filter_var($toBeValidated, FILTER_VALIDATE_INT) !== FALSE;


            //
            // add errormessage
            //
            if($bool === false) {
                $errorMessages[] = "Only integers are allowed.";
            } else {
                
                if(isset($options['min']) && $options['min'] > $toBeValidated) {
                    $errorMessages[] = "Lower limt: {$options['min']}.";
                }
                if(isset($options['max']) && $options['max'] < $toBeValidated) {
                    $errorMessages[] = "Upper limt: {$options['max']}.";
                }
            }
        }
        
        return count($errorMessages) > 0 ? implode(' \n', $errorMessages) : null;
    }




    /**
     * Validates strings. More meaningfull if options minLength, maxLength are
     * given.
     *
     * 
     * @param string
     * @param array with options, minLength, maxLength
     * @return string|null errormessages
     */
    protected function validateString($toBeValidated, array $options = array()) {

        //
        // initialize return value
        //
        $errorMessages = array();



        //
        // check options
        //
        if(array_key_exists('minLength', $options)) {
            $filterOptions = array(
                'options' => array(
                    'min_range' => 1,
                ),
            );
            $valid = filter_var($options['minLength'], FILTER_VALIDATE_INT, $filterOptions) !== FALSE;

            if(!$valid) {
                throw new InvalidArgumentException(
                    'Wrong lower limit.
                ');
            }
        }

        if(array_key_exists('maxLength', $options)) {
            $filterOptions = array(
                'options' => array(
                    'min_range' => 1,
                ),
            );

            $valid = filter_var($options['maxLength'], FILTER_VALIDATE_INT, $filterOptions) !== FALSE;

            if(!$valid) {
                throw new InvalidArgumentException(
                    'Wrong upper limit. Maybe bigger than PHP_INT_MAX on this system.
                ');
            }
        }



        //
        // add errormessage
        //
        if(!($toBeValidated === null || is_string($toBeValidated))) {
            $errorMessages[] = "Only strings are allowed.";
        } else {
            if(isset($options['minLength']) && strlen($toBeValidated) < $options['minLength']) {
                $errorMessages[] = "Min length {$options['minLength']}.";
            }
            if(isset($options['maxLength']) && strlen($toBeValidated) > $options['maxLength']) {
                $errorMessages[] = "Max length {$options['maxLength']}.";
            }
        }



        return count($errorMessages) > 0 ? implode(' \n', $errorMessages) : null;
    }



    /**
     * Alias for validateString
     *
     * @param string
     * @param array with options, minLength, maxLength
     * @return string|null errormessages
     */
    protected function validateText($toBeValidated, array $options = array()) {
        return  $this->validateString($toBeValidated, $options);
    }



    /**
     * Alias for validateString with option minLength = 1: ("required")
     * with errorMessage "Required."
     *
     * 
     * @param string
     * @param array with options, minLength, maxLength
     * @return string|null errormessages
     */
    protected function validateRequired($toBeValidated, array $options = array()) {

        //
        // ignore given options
        //
        $options = array(
            'minLength' => '1',
        );


        //
        // validate
        //
        $errorMessage = $this->validateString($toBeValidated, $options);



        //
        // change errormessage if given
        //
        if($errorMessage !== null) {
            $errorMessage = 'Required.';
        }

        return $errorMessage;
    }



    /**
     * Validates the string to be a valid E-mail.
     * Options don't exist.
     * 
     * @param string
     * @param array with options
     * @return string|null errormessages
     */
    protected function validateEmail($toBeValidated, array $options = array()) {

        //
        // initialise error messages
        //
        $errorMessages = array();


        //
        // test
        //
        $bool = // only validate if value is set!
                $toBeValidated === null || (is_string($toBeValidated) && strlen($toBeValidated) == 0)

                ||
                // validation of email
                filter_var($toBeValidated, FILTER_VALIDATE_EMAIL)
        ;



        //
        // add errormessage
        //
        if($bool === false) {
            $errorMessages[] = "This is not a valid Email!";
        }

        return count($errorMessages) > 0 ? implode(' \n', $errorMessages) : null;
    }



    /**
     * Validates the string to be a valid Url.
     * Options are ignored.
     * 
     * @param string
     * @param array with options
     * @return string|null errormessages
     */
    protected function validateUrl(TableColumn $tableColumn) {

        //
        // initialise error messages
        //
        $errorMessages = array();


        //
        // test
        //
        $bool = // only validate if value is set!
                $toBeValidated === null || (is_string($toBeValidated) && strlen($toBeValidated) == 0)

                ||
                // validation of url
                filter_var($toBeValidated, FILTER_VALIDATE_URL)
        ;


        //
        // add errormessage
        //
        if($bool === false) {
            $errorMessages[] = "This is not a valid URL!";
        }

        return count($errorMessages) > 0 ? implode(' \n', $errorMessages) : null;
    }
}
