<?php
/**
 * Webservice xml validator
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is protected by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2015 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 */

namespace Sso\WebserviceBundle\Validator;

use Sso\WebserviceBundle\Error\Storage as ErrorStorage;
use Sso\WebserviceBundle\Error\Type\External as ExternalError;

/**
 * Xml validator
 * 
 * Validates whole xml or parts of a xml structure
 */
class Xml
{

    /**
     * Error storage
     * 
     * @var ErrorStorage
     */
    protected $errors;

    /**
     * Validate xml
     * 
     * @param \SimpleXMLElement $xml The simple xml object, to validate
     * @param \SimpleXMLElement $xmlValid The simple xml object of the xml structure to validate against
     * @param array $path You may add an array of strings for path representation to get better error messages
     * @return bool True on success, false otherwise
     */
    public function validate(\SimpleXMLElement $xml, \SimpleXMLElement $xmlValid, $path = array())
    {
        $path[] = $xml->getName();
        return $this->_validateElements($xmlValid, $xml, $path);
    }

    /**
     * Converts xml-string to SimpleXMLElement
     * 
     * @param string $xml
     * @return \SimpleXMLElement
     */
    public function strToXml($xml)
    {
        $xml = 0 === stripos($xml, '<?xml') ? $xml : '<?xml version="1.0" encoding="utf-8"?><root>' . $xml . '</root>';
        return simplexml_load_string($xml);
    }

    /**
     * Validate xml elements
     * 
     * @param \SimpleXMLElement $xmlValidElements
     * @param \SimpleXMLElement $xmlElements
     */
    protected function _validateElements(\SimpleXMLElement $xmlValidElements, \SimpleXMLElement $xmlElements, $path = array())
    {
        $valid = true;

        // Check required elements
        if (!$this->_validateRequired($xmlValidElements, $xmlElements, $path)) {
            $valid = false;
        }

        // Check existing elements
        foreach ($xmlElements->children() as $key => $value) {
            if (!property_exists($xmlValidElements, $key)) {
                $valid = false;
                $this->errors()->addError(new ExternalError('x5', 'xmlElementInvalid', 'Xml-element "' . $this->buildPath($path, $key) . '" is not allowed in this context.'));
                continue;
            }

            // Check number of elements
            $xmlValidElement = $xmlValidElements->$key;
            if ((!isset($xmlValidElement['multiple']) || $xmlValidElement['multiple'] == false) && count($xmlValidElements->$key) != count($xmlElements->$key)) {
                $valid = false;
                $this->errors()->addError(new ExternalError('x6', 'xmlElementCount', 'Number of xml-element "' . $this->buildPath($path, $key) . '" should be ' . count($xmlValidElements->$key) . ' but is ' . count($xmlElements->$key) . '.'));
                continue;
            }
            $valid = $this->_validateElements($xmlValidElements->$key, $xmlElements->$key, $this->buildPath($path, $key, true)) && $valid;
        }
        return $valid;
    }

    /**
     * Check for required elements
     * 
     * @param \SimpleXMLElement $xmlValidElements
     * @param \SimpleXMLElement $xmlElements
     * @return boolean
     */
    protected function _validateRequired(\SimpleXMLElement $xmlValidElements, \SimpleXMLElement $xmlElements, $path)
    {
        $valid = true;
        foreach ($xmlValidElements->children() as $key => $value) {
            if (isset($value['required']) && $value['required'] == true && !property_exists($xmlElements, $key)) {
                $valid = false;
                $this->errors()->addError(new ExternalError('x4', 'xmlElementNotFound', 'Required xml-element "' . $this->buildPath($path, $key) . '" does not exist.'));
            }
        }
        return $valid;
    }

    /**
     * Create path to xml-element
     * 
     * @param array $path
     * @param string $key
     * @param bool $asArray
     * @return array|string
     */
    protected function buildPath($path, $key, $asArray = false)
    {
        $currentPath = $path;
        $currentPath[] = $key;
        return $asArray ? $currentPath : implode(' -> ', $currentPath);
    }

    /**
     * Get error storage
     * 
     * @return ErrorStorage
     */
    public function errors()
    {
        return $this->errors ? $this->errors : ($this->errors = new ErrorStorage());
    }

}
