<?php

namespace XmlStroller;

require_once (dirname(__FILE__).'/xmlParser.php');

class XmlStroller
{
    private $xmlNode;
    private $fncList = array(
        "empty" => false,
        "first-child" => false,
        "first-of-type" => false,
        "last-child" => false,
        "last-of-type" => false,
        "not" => true,
        "nth-child" => true,
        "nth-last-child" => true,
        "nth-last-of-type" => true,
        "nth-of-type" => true,
        "only-of-type" => false,
        "only-child" => false,
        "root" => false
    );

    /**
     * Create new Stroller for xml $data
     * @param $data mixed xml data to parse
	 * if $data is a string, implement SimpleXmlIterator to evaluate it
	 * if $data is a SimpleXmlIterator
    **/
    public function __construct($data)
    {
		if (is_string($data))
			$this->xmlNode = XmlNode::parse($data);
		else if ($data instanceof XmlNode)
			$this->xmlNode = clone ($data);
		else if ($data instanceof \SimpleXMLElement)
			$this->xmlNode = $data;
		else
			throw new \Exception("Unrecognized parameter");
    }

    private function getTokens($str)
    {
        $tokens = preg_split("/([^A-Za-z0-9\*\-\.:])+/", $str, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
        $tokens_final = array();
        $nbTokens = count($tokens);
        for ($i = 0; $i < $nbTokens; $i++)
        {
			$tokens_final[] = $tokens[$i][0];
            if ($i < $nbTokens -1)
            {
                $tokenOffsetRight = strlen($tokens[$i][0]) + $tokens[$i][1];
                if ($tokenOffsetRight < $tokens[$i +1][1])
                {
                    $token = substr($str, $tokenOffsetRight, $tokens[$i +1][1] - $tokenOffsetRight);
					$t = trim($token);
					if (ltrim($token) != $token)
						$tokens_final[] = " ";
                    if (!empty($t))
						$tokens_final[] = $t;
					if (rtrim($token) != $token)
						$tokens_final[] = " ";
                }
            }
        }
        $tokens_final[] = ',';
        return $tokens_final;
    }

    private function compile($str)
    {
        $tokens = $this->getTokens($str);
        $result = array();
        $currentBranch = array();
        $nbTokens = count($tokens);
        $childTokenDictionnary = array(">", "+", "~", " ");
		$inAttribute = false;
		$attrs = array();
        $inToken = false;
		$inParam = false;

        for ($i=0; $i < $nbTokens; $i++)
        {
            if ($tokens[$i] == ',')
            {
				if (!empty($attrs))
					$currentBranch[] = array('', array( "value" => "", "fnc" => array(), "attr" => $attrs ));
				$attrs = false;
                $result[] = $currentBranch;
                $currentBranch = array();
            }
			else if ($tokens[$i] == '[' && $inAttribute)
			{
				$expected = ']';
				if (!$inAttribute["key"])
					$expected = 'key';
				else if (!$inAttribute["sep"])
					$expected = 'separator or \']\'';
				else if (!$inAttribute["value"])
					$expected = 'value';
				throw new \Exception("Unexpected token '[', expected {$expected}");
			}
			else if ($tokens[$i] == '[' && !$inAttribute)
			{
				$params = explode(':', $tokens[$i]);
				$inAttribute = array( "key" => null, "sep" => false, "value" => null );
			}
			else if ($inAttribute)
			{
				if ($tokens[$i] == ']')
				{
					if (!$inAttribute["key"])
						throw new \Exception("Unexpected token ']', expected name");
					if ($inAttribute["sep"] !== false && $inAttribute["value"] === null)
						throw new \Exception("Unexpected token ']', expected value");
					if ($inToken !== false)
						$attrs[] = $inAttribute;
					else
						$currentBranch[count($currentBranch) -1][1]["attr"][] = $inAttribute;
					$inAttribute = false;
				}
				else
				{
					if (!$inAttribute["key"])
						$inAttribute["key"] = $tokens[$i];
					else if (!$inAttribute["sep"])
						$inAttribute["sep"] = $tokens[$i];
					else if (!$inAttribute["value"])
						$inAttribute["value"] = $tokens[$i];
				}
			}
            else if (in_array($tokens[$i], $childTokenDictionnary) || empty(trim($tokens[$i])))
            {
                if ($inToken && !empty(trim($tokens[$i])))
                    throw new \Exception("Unexpected token '{$tokens[$i]}', expected name");
                elseif ($inToken)
                    continue;
                $inToken = empty(trim($tokens[$i])) ? '' : $tokens[$i];
            }
            else
            {
				$params = explode(':', $tokens[$i]);
                $currentBranch[] = array($inToken === false ? '' : $inToken, array("value" => $params[0], "fnc" => array_slice($params, 1), "attr" => $attrs));
                $inToken = false;
				$attrs = array();
            }
        }
		if ($attrs !== false)
			$result[count($result) -1] = array('', array( "value" => "", "fnc" => array(), "attr" => $attrs ));
        return $result;
    }

	private function checkAttr($item, $attr)
	{
		$attributes = $item->attributes();
		if ($attr["sep"] == "=")
			return isset($attributes[$attr["key"]]) && $attributes[$attr["key"]] == $attr["value"];
		else if ($attr["sep"] === false)
			return isset($attributes[$attr["key"]]);
		return false;
	}

    private function checkQuery($items, &$result, $criteria)
    {
        if (empty($criteria))
        {
            $result[] = $items;
            return;
        }
        $childCriteria = array_slice($criteria, 1);
        foreach ($items as $i)
        {
            //Check if items match $criteria[0]
            if (($criteria[0][0] == '' || $criteria[0][0] == '>') && ($i->getName() == $criteria[0][1]["value"] || $criteria[0][1]["value"] == '' || $criteria[0][1]["value"] == '*'))
            {
                foreach ($criteria[0][1]["fnc"] as $fnc)
                {
                    if (!array_key_exists($fnc, $this->fncList))
                        throw new \Exception("Unexpected token '{$fnc}', expected function name");
                    //TODO check fnc parameters
                    //TODO exec fnc
                }
				$attr = true;
				foreach ($criteria[0][1]["attr"] as $attr)
				{
					if (!$this->checkAttr($i, $attr))
					{
						$attr = false;
						break;
					}
				}
				if ($attr)
					$this->checkQuery($i, $result, $childCriteria);
            }
            else if ($criteria[0][0] == '')
                $this->checkQuery($i, $result, $criteria);
            //TODO ~, +
        }
    }

    /**
     * Find in xml the request
    **/
    public function select($criteria_str)
    {
        $request = $this->compile($criteria_str);
        $result = array();
		$node = $this->xmlNode;
		if (!($node instanceof XmlNode))
			$node = array($node);
        foreach ($request as $i)
            $this->checkQuery($node, $result, $i);
		$r = array();
		foreach ($result as $i)
			$r[] = new self($i);
		return $r;
    }

	public function getAttributes()
	{
		$result = array();
		foreach ($this->xmlNode->attributes() as $i => $j)
			$result[$i] = (string) $j;
		return $result;
	}

	public function attr($key =null, $defaultValue =null)
	{
		if ($key == null)
			return $this->getAttributes();
		if (isset($this->xmlNode->attributes()[$key]))
			return (string)$this->xmlNode->attributes()[$key];
		return $defaultValue;
	}

	public function valueString()
	{
		return $this->value;
	}

	public function __tostring()
	{
		return $this->valueString();
	}

	public function value()
	{
		return (string) $this->xmlNode;
	}

	public function valueFloat()
	{
		return (float) $this->value();
	}

	public function valueDouble()
	{
		return (double) $this->value();
	}

	public function valueInt()
	{
		return (int) $this->value();
	}

    /**
     * Find in xml the request
    **/
    public function __get($name)
    {
        return $this->select($name);
    }

	public function node()
	{
		return $this->xmlNode;
	}
}

