<?php

namespace Fifa\ConnectId\Sdk\ApiModelValidation;

use DateTime;
use Fifa\ConnectId\Sdk\Registration\ValidationException;
use InvalidArgumentException;
use LogicException;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;

class ApiModelValidator
{
    /**
     * Method that throws exception when validationSubject is null
     *
     * @param mixed $validationSubject
     * @param string $validationSubjectName
     * @return void
     * @throws ValidationException
     */
    public static function cannotBeNull($validationSubject, string $validationSubjectName)
    {
        if(is_null($validationSubject))
        {
            throw new ValidationException(sprintf("%s cannot be null", $validationSubjectName));
        }
    }

    /**
     * Method that throws exception when string is null or empty
     *
     * @param string|null $textValue
     * @param string $validationSubjectName
     * @return void
     * @throws ValidationException
     */
    public static function cannotBeNullOrWhitespace(?string $textValue, string $validationSubjectName)
    {
        if(self::isNullOrWhitespace($textValue))
        {
            throw new ValidationException(sprintf("%s cannot be null or empty", $validationSubjectName));
        }
    }

    /**
     * Method that checks if string is null or empty
     *
     * @param string|null $textValue
     * @return bool
     */
    public static function isNullOrWhitespace(?string $textValue): bool
    {
        return strlen(trim($textValue)) == 0;
    }

    /**
     * Method that throws exception when string does not match pattern
     *
     * @param string|null $textValue
     * @param string|null $pattern
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     */
    public static function mustMatchPattern(?string $textValue, ?string $pattern, string $validationSubjectName)
    {
        if(is_null($pattern))
        {
            throw new InvalidArgumentException(sprintf("%s (pattern) cannot be null", $validationSubjectName));
        }
        self::cannotBeNull($textValue, $validationSubjectName);

        if(!preg_match($pattern, $textValue))
        {
            throw new ValidationException(sprintf("[%s] Validation against pattern: %s failed. Provided value: %s", $validationSubjectName, $pattern, $textValue));
        }
    }

    /**
     * Method that throws exception if value is null or less than specified minimum value.
     *
     * @param DateTime|int|float $value
     * @param DateTime|int|float $minValue
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     */
    public static function mustBeGreaterOrEqual($value, $minValue, string $validationSubjectName)
    {
        if(is_null($minValue))
        {
            throw new InvalidArgumentException(sprintf("%s (minValue) cannot be null", $validationSubjectName));
        }

        if(is_null($value) || $value < $minValue)
        {
            $minValueString = self::formatAsString($minValue);
            $valueString = self::formatAsString($value);
            throw new ValidationException(sprintf("%s must be greater or equal to %s, but was %s", $validationSubjectName, $minValueString, $valueString));
        }
    }

    /**
     * Method that accepts null and throws exception if value is less than specified minimum value.
     *
     * @param DateTime|int|float $value
     * @param DateTime|int|float $minValue
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     */
    public static function mustBeNullOrGreaterOrEqual($value, $minValue, string $validationSubjectName)
    {
        if(is_null($value))
        {
            return;
        }

        self::mustBeGreaterOrEqual($value, $minValue, $validationSubjectName);
    }

    /**
     * Method that throws exception if value is null or greater than specified maximum value.
     *
     * @param DateTime|int|float $value
     * @param DateTime|int|float $maxValue
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     */
    public static function mustBeLessOrEqual($value, $maxValue, string $validationSubjectName)
    {
        if(is_null($maxValue))
        {
            throw new InvalidArgumentException(sprintf("%s (maxValue) cannot be null", $validationSubjectName));
        }

        if(is_null($value) || $value > $maxValue)
        {
            $maxValueString = self::formatAsString($maxValue);
            $valueString = self::formatAsString($value);
            throw new ValidationException(sprintf("%s must be less or equal to %s, but was %s", $validationSubjectName, $maxValueString, $valueString));
        }
    }

    /**
     * Method that accepts null and throws exception if value is greater than specified maximum value.
     *
     * @param DateTime|int|float $value
     * @param DateTime|int|float $maxValue
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     */
    public static function mustBeNullOrLessOrEqual($value, $maxValue, string $validationSubjectName)
    {
        if(is_null($value))
        {
            return;
        }

        self::mustBeLessOrEqual($value, $maxValue, $validationSubjectName);
    }

    /**
     * Method that accepts null and throws exception when validationSubject is not correctly validated.
     *
     * @param mixed|null $validationSubject
     * @param string|null $validatorClass
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     * @noinspection PhpDocRedundantThrowsInspection
     */
    public static function validateAllowNull($validationSubject, ?string $validatorClass, string $validationSubjectName)
    {
        if(!is_null($validationSubject))
        {
            self::invokeValidationMethodForValidator($validatorClass, $validationSubject, $validationSubjectName);
        }
    }

    /**
     * Method that throws exception when validationSubject is null or not correctly validated.
     *
     * @param mixed $validationSubject
     * @param string|null $validatorClass
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     */
    public static function validateDoNotAllowNull($validationSubject, ?string $validatorClass, string $validationSubjectName)
    {
        self::cannotBeNull($validationSubject, $validationSubjectName);
        self::invokeValidationMethodForValidator($validatorClass, $validationSubject, $validationSubjectName);
    }

    /**
     * Method that throws exception when collection of validation subjects is null or any subject is not correctly validated.
     *
     * @param array|null $collectionOfValidationSubjects
     * @param string|null $validatorClass
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     */
    public static function validateAllInCollection(?array $collectionOfValidationSubjects, ?string $validatorClass, string $validationSubjectName)
    {
        if(is_null($collectionOfValidationSubjects))
        {
            throw new ValidationException(sprintf("%s cannot be null", $validationSubjectName));
        }

        $count = 0;
        foreach ($collectionOfValidationSubjects as $validationSubject) {
            self::invokeValidationMethodForValidator($validatorClass, $validationSubject, $validationSubjectName."[".$count."]");
            $count++;
        }
    }

    /**
     * Method that accepts null collection of validation subjects and throws exception when any subject is not correctly validated.
     *
     * @param array|null $collectionOfValidationSubjects
     * @param string|null $validatorClass
     * @param string $validationSubjectName
     * @return void
     * @throws InvalidArgumentException
     * @throws ValidationException
     */
    public static function validateAllInCollectionAllowNull(?array $collectionOfValidationSubjects, ?string $validatorClass, string $validationSubjectName)
    {
        if(is_null($collectionOfValidationSubjects))
        {
            return;
        }

        self::validateAllInCollection($collectionOfValidationSubjects, $validatorClass, $validationSubjectName);
    }

    /**
     * Method that throws exception when textValue does not match any enum value
     *
     * @param string|null $textValue
     * @param array $enumValidValues
     * @param string $validationSubjectName
     * @param bool $ignoreCase
     * @return void
     * @throws ValidationException
     */
    public static function mustMatchEnumValue(?string $textValue, array $enumValidValues, string $validationSubjectName, bool $ignoreCase = true)
    {
        if(!$enumValidValues) {
            throw new InvalidArgumentException(sprintf("%s (enumValidValues) cannot be null or empty", $validationSubjectName));
        }

        foreach ($enumValidValues as $validValue)
        {
            if($ignoreCase ? strcasecmp($validValue, $textValue) == 0 : strcmp($validValue, $textValue) == 0)
            {
                return;
            }
        }

        throw new ValidationException(sprintf("%s was %s, but must be one of: %s", $validationSubjectName, $textValue, join(', ', $enumValidValues)));
    }

    /**
     * Method that accepts null textValue and throws exception when textValue does not match any enum value
     *
     * @param string|null $textValue
     * @param array $enumValidValues
     * @param string $validationSubjectName
     * @param bool $ignoreCase
     * @return void
     * @throws ValidationException
     */
    public static function mustBeNullOrMatchEnumValue(?string $textValue, array $enumValidValues, string $validationSubjectName, bool $ignoreCase = true)
    {
        if(is_null($textValue))
        {
            return;
        }

        self::mustMatchEnumValue($textValue, $enumValidValues, $validationSubjectName, $ignoreCase);
    }

    /**
     * Method that invokes validation method for specified validator class, passing validation subject and its name
     *
     * @param string|null $validatorClass
     * @param mixed $validationSubject
     * @param string $validationSubjectName
     * @return void
     */
    public static function invokeValidationMethodForValidator(?string $validatorClass, $validationSubject, string $validationSubjectName)
    {
        if(self::isNullOrWhitespace($validatorClass))
        {
            throw new InvalidArgumentException(sprintf("%s (validatorClass) cannot be null or whitespace", $validationSubjectName));
        }

        try {
            $method = self::getValidationMethodForValidator($validatorClass);
            $method->invoke(null, $validationSubject, $validationSubjectName);
        } catch (ReflectionException $exception) {
            throw new LogicException("Couldn't get validation method for object of: ".$validatorClass." Exception: ".$exception);
        }
    }

    /**
     * Method that formats DateTime object as Y-m-d, but if object is int or float it is cast to string
     *
     * @param DateTime|int|float $object
     * @return string
     * @throws InvalidArgumentException
     */
    public static function formatAsString($object): string
    {
        if(is_A($object, 'DateTime')) {
            return $object->format('Y-m-d');
        }
        return (string) $object;
    }

    /**
     * Method that returns validationSubjectName if provided, otherwise class name of validationSubject
     *
     * @param mixed $validationSubject
     * @param string $validationSubjectName
     * @return string
     */
    public static function resolveValidationSubjectName($validationSubject, string $validationSubjectName): string
    {
        return self::isNullOrWhiteSpace($validationSubjectName) ? get_class($validationSubject) : $validationSubjectName;
    }

    /**
     * @throws ReflectionException
     */
    private static function getValidationMethodForValidator($validatorClass): ReflectionMethod
    {
        $modelReflector = new ReflectionClass($validatorClass);
        $method = $modelReflector->getMethod("validate");

        $parameters = $method->getParameters();
        $parametersCount = count($parameters);
        if($parametersCount != 2) {
            throw new LogicException("There are ".$parametersCount." parameters in validate method for ".$validatorClass." but there should be exactly 2");
        }

        return $method;
    }
}