<?php

/**
 * This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT License
 */

/**
 * XML parser. Converts data between associative array and XML formats
 *
 * @author     Francois Zaninotto
 * @package    propel.runtime.parser
 */
class PropelXMLParser extends PropelParser
{

    /**
     * Converts data from an associative array to XML.
     *
     * @param array  $array           Source data to convert
     * @param string $rootElementName Name of the root element of the XML document
     * @param string $charset         Character set of the input data. Defaults to UTF-8.
     *
     * @return string Converted data, as an XML string
     */
    public function fromArray($array, $rootElementName = 'data', $charset = null)
    {
        $rootNode = $this->getRootNode($rootElementName);
        $this->arrayToDOM($array, $rootNode, $charset, false);

        return $rootNode->ownerDocument->saveXML();
    }

    public function listFromArray($array, $rootElementName = 'data', $charset = null)
    {
        $rootNode = $this->getRootNode($rootElementName);
        $this->arrayToDOM($array, $rootNode, $charset, true);

        return $rootNode->ownerDocument->saveXML();
    }

    /**
     * Create a DOMDocument and get the root DOMNode using a root element name
     *
     * @param string $rootElementName The Root Element Name
     *
     * @return DOMNode The root DOMNode
     */
    protected function getRootNode($rootElementName = 'data')
    {
        $xml = new DOMDocument('1.0', 'UTF-8');
        $xml->preserveWhiteSpace = false;
        $xml->formatOutput = true;
        $rootElement = $xml->createElement($rootElementName);
        $xml->appendChild($rootElement);

        return $rootElement;
    }

    /**
     * Alias for PropelXMLParser::fromArray()
     *
     * @param array  $array           Source data to convert
     * @param string $rootElementName Name of the root element of the XML document
     * @param string $charset         Character set of the input data. Defaults to UTF-8.
     *
     * @return string Converted data, as an XML string
     */
    public function toXML($array, $rootElementName = 'data', $charset = null)
    {
        return $this->fromArray($array, $rootElementName, $charset);
    }

    /**
     * Alias for PropelXMLParser::listFromArray()
     *
     * @param array  $array           Source data to convert
     * @param string $rootElementName Name of the root element of the XML document
     * @param string $charset         Character set of the input data. Defaults to UTF-8.
     *
     * @return string Converted data, as an XML string
     */
    public function listToXML($array, $rootElementName = 'data', $charset = null)
    {
        return $this->listFromArray($array, $rootElementName, $charset);
    }

    /**
     * @param array      $array
     * @param DOMElement $rootElement
     * @param string     $charset
     * @param boolean    $removeNumbersFromKeys
     *
     * @return DOMElement
     */
    protected function arrayToDOM($array, $rootElement, $charset = null, $removeNumbersFromKeys = false)
    {
        foreach ($array as $key => $value) {
            if ($removeNumbersFromKeys) {
                $key = preg_replace('/[^a-z]/i', '', $key);
            }
            $element = $rootElement->ownerDocument->createElement($key);
            if (is_array($value)) {
                if (!empty($value)) {
                    $element = $this->arrayToDOM($value, $element, $charset);
                }
            } elseif (is_string($value)) {
                $charset = $charset ? $charset : 'utf-8';
                if (function_exists('iconv') && strcasecmp($charset, 'utf-8') !== 0 && strcasecmp($charset, 'utf8') !== 0) {
                    $value = iconv($charset, 'UTF-8', $value);
                }
                $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8');
                $child = $element->ownerDocument->createCDATASection($value);
                $element->appendChild($child);
            } else {
                $child = $element->ownerDocument->createTextNode($value);
                $element->appendChild($child);
            }
            $rootElement->appendChild($element);
        }

        return $rootElement;
    }

    /**
     * Converts data from XML to an associative array.
     *
     * @param string $data Source data to convert, as an XML string
     *
     * @return array Converted data
     */
    public function toArray($data)
    {
        $doc = new DomDocument('1.0', 'UTF-8');
        $doc->loadXML($data);
        $element = $doc->documentElement;

        return $this->convertDOMElementToArray($element);
    }

    /**
     * Alias for PropelXMLParser::toArray()
     *
     * @param string $data Source data to convert, as an XML string
     *
     * @return array Converted data
     */
    public function fromXML($data)
    {
        return $this->toArray($data);
    }

    /**
     * @param DOMNode $data
     *
     * @return array
     */
    protected function convertDOMElementToArray(DOMNode $data)
    {
        $array = array();
        $elementNames = array();
        foreach ($data->childNodes as $element) {
            if ($element->nodeType == XML_TEXT_NODE) {
                continue;
            }
            $name = $element->nodeName;
            if (isset($elementNames[$name])) {
                if (isset($array[$name])) {
                    // change the first 'book' to 'book0'
                    $array[$name . $elementNames[$name]] = $array[$name];
                    unset($array[$name]);
                }
                $elementNames[$name] += 1;
                $index = $name . $elementNames[$name];
            } else {
                $index = $name;
                $elementNames[$name] = 0;
            }
            if ($element->hasChildNodes() && !$this->hasOnlyTextNodes($element)) {
                $array[$index] = $this->convertDOMElementToArray($element);
            } elseif ($element->hasChildNodes() && $element->firstChild->nodeType == XML_CDATA_SECTION_NODE) {
                $array[$index] = htmlspecialchars_decode($element->firstChild->textContent);
            } elseif (!$element->hasChildNodes()) {
                $array[$index] = null;
            } else {
                $array[$index] = $element->textContent;
            }
        }

        return $array;
    }

    /**
     * @param DomNode $node
     *
     * @return boolean
     */
    protected function hasOnlyTextNodes(DomNode $node)
    {
        foreach ($node->childNodes as $childNode) {
            if ($childNode->nodeType != XML_CDATA_SECTION_NODE && $childNode->nodeType != XML_TEXT_NODE) {
                return false;
            }
        }

        return true;
    }
}
