<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link       http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Authentication\Validator;

use Traversable;
use Zend\Authentication\Adapter\ValidatableAdapterInterface;
use Zend\Authentication\AuthenticationService;
use Zend\Authentication\Result;
use Zend\Authentication\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Validator\AbstractValidator;

/**
 * Authentication Validator
 */
class Authentication extends AbstractValidator
{
    /**
     * Error codes
     * @const string
     */
    const IDENTITY_NOT_FOUND = 'identityNotFound';
    const IDENTITY_AMBIGUOUS = 'identityAmbiguous';
    const CREDENTIAL_INVALID = 'credentialInvalid';
    const UNCATEGORIZED      = 'uncategorized';
    const GENERAL            = 'general';

    /**
     * Error Messages
     * @var array
     */
    protected $messageTemplates = [
        self::IDENTITY_NOT_FOUND => 'Invalid identity',
        self::IDENTITY_AMBIGUOUS => 'Identity is ambiguous',
        self::CREDENTIAL_INVALID => 'Invalid password',
        self::UNCATEGORIZED      => 'Authentication failed',
        self::GENERAL            => 'Authentication failed',
    ];

    /**
     * Authentication Adapter
     * @var ValidatableAdapterInterface
     */
    protected $adapter;

    /**
     * Identity (or field)
     * @var string
     */
    protected $identity;

    /**
     * Credential (or field)
     * @var string
     */
    protected $credential;

    /**
     * Authentication Service
     * @var AuthenticationService
     */
    protected $service;

    /**
     * Sets validator options
     *
     * @param mixed $options
     */
    public function __construct($options = null)
    {
        if ($options instanceof Traversable) {
            $options = ArrayUtils::iteratorToArray($options);
        }

        if (is_array($options)) {
            if (array_key_exists('adapter', $options)) {
                $this->setAdapter($options['adapter']);
            }
            if (array_key_exists('identity', $options)) {
                $this->setIdentity($options['identity']);
            }
            if (array_key_exists('credential', $options)) {
                $this->setCredential($options['credential']);
            }
            if (array_key_exists('service', $options)) {
                $this->setService($options['service']);
            }
        }
        parent::__construct($options);
    }

    /**
     * Get Adapter
     *
     * @return ValidatableAdapterInterface
     */
    public function getAdapter()
    {
        return $this->adapter;
    }

    /**
     * Set Adapter
     *
     * @param  ValidatableAdapterInterface $adapter
     * @return Authentication
     */
    public function setAdapter(ValidatableAdapterInterface $adapter)
    {
        $this->adapter = $adapter;

        return $this;
    }

    /**
     * Get Identity
     *
     * @return mixed
     */
    public function getIdentity()
    {
        return $this->identity;
    }

    /**
     * Set Identity
     *
     * @param  mixed          $identity
     * @return Authentication
     */
    public function setIdentity($identity)
    {
        $this->identity = $identity;

        return $this;
    }

    /**
     * Get Credential
     *
     * @return mixed
     */
    public function getCredential()
    {
        return $this->credential;
    }

    /**
     * Set Credential
     *
     * @param  mixed          $credential
     * @return Authentication
     */
    public function setCredential($credential)
    {
        $this->credential = $credential;

        return $this;
    }

    /**
     * Get Service
     *
     * @return AuthenticationService
     */
    public function getService()
    {
        return $this->service;
    }

    /**
     * Set Service
     *
     * @param  AuthenticationService $service
     * @return Authentication
     */
    public function setService(AuthenticationService $service)
    {
        $this->service = $service;

        return $this;
    }

    /**
     * Is Valid
     *
     * @param  mixed $value
     * @param  array $context
     * @return bool
     */
    public function isValid($value = null, $context = null)
    {
        if ($value !== null) {
            $this->setCredential($value);
        }

        if (($context !== null) && array_key_exists($this->identity, $context)) {
            $identity = $context[$this->identity];
        } else {
            $identity = $this->identity;
        }
        if (!$this->identity) {
            throw new Exception\RuntimeException('Identity must be set prior to validation');
        }

        if (($context !== null) && array_key_exists($this->credential, $context)) {
            $credential = $context[$this->credential];
        } else {
            $credential = $this->credential;
        }

        if (!$this->adapter) {
            throw new Exception\RuntimeException('Adapter must be set prior to validation');
        }
        $this->adapter->setIdentity($identity);
        $this->adapter->setCredential($credential);

        if (!$this->service) {
            throw new Exception\RuntimeException('AuthenticationService must be set prior to validation');
        }
        $result = $this->service->authenticate($this->adapter);

        if ($result->getCode() != Result::SUCCESS) {
            switch ($result->getCode()) {
                case Result::FAILURE_IDENTITY_NOT_FOUND:
                    $this->error(self::IDENTITY_NOT_FOUND);
                    break;
                case Result::FAILURE_CREDENTIAL_INVALID:
                    $this->error(self::CREDENTIAL_INVALID);
                    break;
                case Result::FAILURE_IDENTITY_AMBIGUOUS:
                    $this->error(self::IDENTITY_AMBIGUOUS);
                    break;
                case Result::FAILURE_UNCATEGORIZED:
                    $this->error(self::UNCATEGORIZED);
                    break;
                default:
                    $this->error(self::GENERAL);
            }

            return false;
        }

        return true;
    }
}
