<?php
namespace Fifa\ConnectId\Sdk\Util\XmlDeserializer;

use InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
use SimpleXMLElement;

/**
 * Class XmlDeserializer
 * @package Fifa\ConnectId\Sdk\Util\XmlDeserializer
 */
class XmlDeserializer implements XmlDeserializerInterface
{
    /**
     * @var string
     */
    private $targetClass;

    /**
     * @var string|null
     */
    private $namespace;

    /**
     * @var string[]
     */
    private $mappings;

    /**
     * @var callable[]
     */
    private $customConstructors;

    /**
     * XmlDeserializer constructor.
     * @param string $targetClass
     * @param string[] $mappings
     * @param callable[] $customConstructors
     */
    public function __construct(string $targetClass, array $mappings, array $customConstructors = [])
    {
        $this->targetClass = $targetClass;
        $this->mappings = $mappings;
        $this->customConstructors = $customConstructors;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return sprintf('Target Class: %s', $this->targetClass);
    }

    /**
     * @param string $xmlContent
     * @return object
     * @throws ReflectionException
     * @throws InvalidArgumentException
     */
    public function deserialize(string $xmlContent)
    {
        $data = self::tryDeserialize($xmlContent);

        $namespaces = $data->getNamespaces();
        $namespaces = array_values($namespaces);
        $this->namespace = (sizeof($namespaces) > 0) ? $namespaces[0] : null;

        $targetInstance = $this->deserializeType($this->targetClass, $data);
        RequiredPropertiesValidator::validate($targetInstance);

        return $targetInstance;
    }

    /**
     * @param string $xmlContent
     * @return SimpleXMLElement
     * @throws ReflectionException
     */
    private static function tryDeserialize(string $xmlContent): SimpleXMLElement
    {
        libxml_use_internal_errors(true);
        $data = simplexml_load_string($xmlContent);
        if($data === FALSE) {
            $errorMessage = libxml_get_last_error()->message;
            libxml_use_internal_errors(false);
            throw new ReflectionException($errorMessage);
        }
        libxml_use_internal_errors(false);
        return $data;
    }

    /**
     * @param string $targetClass
     * @param SimpleXMLElement $xmlElement
     * @return object
     * @throws ReflectionException
     */
    private function deserializeType(string $targetClass, SimpleXMLElement $xmlElement)
    {
        $reflectionObj = new ReflectionClass($targetClass);

        if (!isset($this->customConstructors[$targetClass])) {
            $targetInstance = $reflectionObj->newInstanceWithoutConstructor();
        }
        else {
            $targetInstance = call_user_func($this->customConstructors[$targetClass], $xmlElement);
        }

        $this->applyProperties($targetInstance, $reflectionObj, $xmlElement);
        $this->applyChildren($targetInstance, $xmlElement);

        return $targetInstance;
    }

    /**
     * @param object $targetInstance
     * @param SimpleXMLElement $xmlElement
     * @throws ReflectionException
     */
    private function applyChildren($targetInstance, SimpleXMLElement $xmlElement)
    {
        $children = $xmlElement->children($this->namespace);
        foreach ($children as $child) {

            $propertyName = $child->getName();
            if (isset($this->mappings[$propertyName])) {
                if (is_array($targetInstance->{$propertyName})) {
                    $targetInstance->{$propertyName}[] = $this->deserializeType($this->mappings[$propertyName], $child);
                }
                else {
                    $targetInstance->{$propertyName} = $this->deserializeType($this->mappings[$propertyName], $child);
                }
            }
        }
    }

    /**
     * @param object $targetInstance
     * @param ReflectionClass $reflectionObj
     * @param SimpleXMLElement $xmlElement
     */
    private function applyProperties($targetInstance, ReflectionClass $reflectionObj, SimpleXMLElement $xmlElement)
    {
        $targetProperties = $reflectionObj->getProperties();
        foreach ($targetProperties as $property) {
            if (isset($xmlElement->attributes()->{$property->getName()})) {
                $targetInstance->{$property->getName()} = $xmlElement->attributes()->{$property->getName()}->__toString();
            }
        }
    }
}