<?php 

namespace NPS\Model;

use NPS\Exception\InvalidXMLException;
use SimpleXMLElement;

/**
 * Class providing XML functions with traversable interface
 * Designed with immutability in mind
 * A node can be either child or text, not both
 * @author Lukas Korinek
 */
class TraversableXMLElement
{

    /**
     * Creates an instance of a new tree root
     * @param string $node Node name
     * @param array $attributes Attributes of the tag
     * @param string $text Text content of the node
     * @return TraversableXMLElement
     */
    public static function create($node, array $attributes = [], $text = '')
    {
        return new self($node, $text, $attributes);
    }

    /**
     * Creates a new instance out of a SimpleXMLElement
     * @param SimpleXMLElement $simple 
     * @return TraversableXMLElement
     */
    public static function fromSimpleXMLElement(SimpleXMLElement $simple)
    {
        $xml = new self(
            $simple->getName(),
            (string)$simple,
            iterator_to_array($simple->attributes())
        );

        foreach($simple->children() as $child)
        {
            $xml = $xml->addChild(self::fromSimpleXMLElement($child));
        }

        return $xml;
    }

    /**
     * Creates a new instance out of a string
     * @param string $str
     * @return TraversableXMLElement
     */
    public static function fromString($str)
    {
        $simple = simplexml_load_string($str);

        if($simple === false)
        {
            throw new InvalidXMLException('Invalid XML string');
        }

        return self::fromSimpleXMLElement($simple);
    }

    //////////////////////////////////////////////////////////////////////
    // Properties

    /**
     * @var string $node Node name
     */
    protected $node;

    /**
     * @var TraversableXMLElement $parent
     */
    protected $parent;

    /**
     * @var array $children 
     */
    protected $children;

    /**
     * @var array $attributes
     */
    protected $attributes;

    /**
     * @var string $text Text node of this element
     */
    protected $text;

    ////////////////////////////////////////////////////////////////////////////
    // Modifiers

    /**
     * @param string $node Node name
     * @param string $text Content of the text node
     * @param array $attributes 
     * @param TraversableXMLElement $parent
     * @param array $children
     */
    protected function __construct($node, $text, array $attributes, TraversableXMLElement $parent = null, array $children = [])
    {
        $this->node = $node;
        $this->text = trim((string)$text);
        $this->parent = $parent;

        $this->children = [];
        foreach($children as $child)
        {
            $this->children[] = new self($child->getNodeName(), $child->getText(), $child->getAttributes(), $this, $child->getChildren());
        }

        $this->attributes = [];
        foreach($attributes as $attr => $value)
        {
            $this->attributes[(string)$attr] = (string)$value;
        }
    }


    /////////////////////////////////////////////////////////////////////////////////

    /**
     * Gets the name of this node
     * @return string
     */
    public function getNodeName()
    {
        return $this->node;
    }

    /**
     * Gets the text of this node
     * @return string
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Gets attributes of this node
     * @return array
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * Gets an attribute
     * @param string $key Attribute name
     * @return string or null on non existing attribute
     */
    public function getAttribute($key)
    {
        return array_key_exists($key, $this->attributes) ? $this->attributes[$key] : null;
    }

    /**
     * Gets the parent of this node
     * @return TraversableXMLElement
     */
    public function getParent()
    {
        return $this->parent;
    }

    /**
     * Determines if this element is the root of a tree
     * @return bool
     */
    public function isRoot()
    {
        return ($this->parent == null);
    }

    /**
     * Gets the siblings of this element
     * @param string $filter
     * @return array
     */
    public function getSiblings($filter = null)
    {
        if(!$this->getParent())
        {
            return [];
        }

        return array_values(
            array_filter(
                $this->getParent()->getChildren(),
                function($child) use ($filter)
                {
                    return $child != $this && (!$filter || $child->getNodeName() == $filter);
                }
            )
        );
    }

    /**
     * Gets the children of this element
     * @param $filter Can filter by node name
     * @return array
     */
    public function getChildren($filter = null)
    {
        if(!$filter)
        {
            return $this->children;
        }

        return array_values( 
            array_filter(
                $this->children, 
                function($child) use ($filter)
                {
                    return $child->getNodeName() == $filter;
                }
            )
        );
    }

    /**
     * Gets a child by a specific filter or the first one in the list
     * @param $filter Can filter by node name
     * @return TraversableXMLElement
     */
    public function getChild($filter = null)
    {
        foreach($this->children as $child)
        {
            if(!$filter || $child->getNodeName() == $filter)
            {
                return $child;
            }
        }

        return null;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Setters

    /**
     * Sets the text content of the element
     * @param string $text
     * @return TraversableXMLElement
     */
    public function setText($text)
    {
        return new self($this->getNodeName(), $text, $this->getAttributes(), $this->getParent(), $this->getChildren());
    }

    /**
     * Adds a new child element
     * @param TraversableXMLElement $element 
     * @return TraversableXMLElement
     */
    public function addChild(TraversableXMLElement $element)
    {
        return new self($this->getNodeName(), $this->getText(), $this->getAttributes(), $this->getParent(), array_merge($this->children, [$element]));
    }

    /**
     * Removes a child element
     * @param TraversableXMLElement $element
     * @return TraversableXMLElement
     */
    public function removeChild(TraversableXMLElement $element)
    {
        $children = array_filter($this->getChildren(), function($child) use ($element){ return $child != $element; });
        return new self($this->getNodeName(), $this->getText(), $this->getAttributes(), $this->getParent(), $children);
    }

    /**
     * Sets an attribute
     * @param string $name Attribute name
     * @param string $value Attribute value
     * @return TraversableXMLElement
     */
    public function setAttribute($name, $value)
    {
        $attributes = $this->attributes;
        $attributes[$name] = $value;

        return new self($this->getNodeName(), $this->getText(), $attributes, $this->getParent(), $this->getChildren());
    }

    /**
     * Removes an attribute
     * @param string $name Attribute name
     */
    public function removeAttribute($name)
    {
        $attributes = $this->attributes;
        unset($attributes[$name]);

        return new self($this->getNodeName(), $this->getText(), $attributes, $this->getParent(), $this->getChildren());
    }

    ///////////////////////////////////////////////////////////////////////////
    // I/O

    /**
     * Makes an XML string output
     * @return string
     */
    public function __toString()
    {
        $childrenString = implode("", $this->children);
        $attrString = implode(' ', 
            array_map(
                function($key, $value)
                { 
                    return $key . '="' . $value . '"'; 
                }, 
                array_keys($this->attributes), 
                $this->attributes
            )
        );

        return sprintf(
            "<%s%s>%s%s</%s>",
            $this->node,
            $attrString ? (' ' . $attrString) : '',
            $this->text,
            $childrenString,
            $this->node
        );
    }

    /**
     * Returns simple XML element based on this traversable element
     * @return SimpleXMLElement
     */
    public function toSimpleXMLElement()
    {
        return simplexml_load_string((string)$this);
    }

}

