<?php namespace Sieve;

include_once 'SieveTree.php';
include_once 'SieveScanner.php';
include_once 'SieveSemantics.php';
include_once 'SieveException.php';

class SieveParser
{
    protected $scanner_;
    protected $script_;
    protected $tree_;
    protected $status_;

    public function __construct($script = null)
    {
        if (isset($script))
            $this->parse($script);
    }

    public function GetParseTree()
    {
        return $this->tree_;
    }

    public function dumpParseTree()
    {
        return $this->tree_->dump();
    }

    public function getScriptText()
    {
        return $this->tree_->getText();
    }

    protected function getPrevToken_($parent_id)
    {
        $childs = $this->tree_->getChilds($parent_id);

        for ($i = count($childs); $i > 0; --$i)
        {
            $prev = $this->tree_->getNode($childs[$i-1]);
            if ($prev->is(SieveToken::Comment|SieveToken::Whitespace))
                continue;

            // use command owning a block or list instead of previous
            if ($prev->is(SieveToken::BlockStart|SieveToken::Comma|SieveToken::LeftParenthesis))
                $prev = $this->tree_->getNode($parent_id);

            return $prev;
        }

        return $this->tree_->getNode($parent_id);
    }

    /*******************************************************************************
     * methods for recursive descent start below
     */
    public function passthroughWhitespaceComment($token)
    {
        return 0;
    }

    public function passthroughFunction($token)
    {
        $this->tree_->addChild($token);
    }

    public function parse($script)
    {
        $this->script_ = $script;

        $this->scanner_ = new SieveScanner($this->script_);

        // Define what happens with passthrough tokens like whitespacs and comments
        $this->scanner_->setPassthroughFunc(
            array(
                $this, 'passthroughWhitespaceComment'
            )
        );

        $this->tree_ = new SieveTree('tree');

        $this->commands_($this->tree_->getRoot());

        if (!$this->scanner_->nextTokenIs(SieveToken::ScriptEnd)) {
            $token = $this->scanner_->nextToken();
            throw new SieveException($token, SieveToken::ScriptEnd);
        }
    }

    protected function commands_($parent_id)
    {
        while (true)
        {
            if (!$this->scanner_->nextTokenIs(SieveToken::Identifier))
                break;

            // Get and check a command token
            $token = $this->scanner_->nextToken();
            $semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));

            // Process eventual arguments
            $this_node = $this->tree_->addChildTo($parent_id, $token);
            $this->arguments_($this_node, $semantics);

            $token = $this->scanner_->nextToken();
            if (!$token->is(SieveToken::Semicolon))
            {
                // TODO: check if/when semcheck is needed here
                $semantics->validateToken($token);

                if ($token->is(SieveToken::BlockStart))
                {
                    $this->tree_->addChildTo($this_node, $token);
                    $this->block_($this_node, $semantics);
                    continue;
                }

                throw new SieveException($token, SieveToken::Semicolon);
            }

            $semantics->done($token);
            $this->tree_->addChildTo($this_node, $token);
        }
    }

    protected function arguments_($parent_id, &$semantics)
    {
        while (true)
        {
            if ($this->scanner_->nextTokenIs(SieveToken::Number|SieveToken::Tag))
            {
                // Check if semantics allow a number or tag
                $token = $this->scanner_->nextToken();
                $semantics->validateToken($token);
                $this->tree_->addChildTo($parent_id, $token);
            }
            else if ($this->scanner_->nextTokenIs(SieveToken::StringList))
            {
                $this->stringlist_($parent_id, $semantics);
            }
            else
            {
                break;
            }
        }

        if ($this->scanner_->nextTokenIs(SieveToken::TestList))
        {
            $this->testlist_($parent_id, $semantics);
        }
    }

    protected function stringlist_($parent_id, &$semantics)
    {
        if (!$this->scanner_->nextTokenIs(SieveToken::LeftBracket))
        {
            $this->string_($parent_id, $semantics);
            return;
        }

        $token = $this->scanner_->nextToken();
        $semantics->startStringList($token);
        $this->tree_->addChildTo($parent_id, $token);
        
        if($this->scanner_->nextTokenIs(SieveToken::RightBracket)) {
            //allow empty lists
            $token = $this->scanner_->nextToken();
            $this->tree_->addChildTo($parent_id, $token);
            $semantics->endStringList();
            return;
        }

        do
        {
            $this->string_($parent_id, $semantics);
            $token = $this->scanner_->nextToken();

            if (!$token->is(SieveToken::Comma|SieveToken::RightBracket))
                throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightBracket));

            if ($token->is(SieveToken::Comma))
                $semantics->continueStringList();

            $this->tree_->addChildTo($parent_id, $token);
        }
        while (!$token->is(SieveToken::RightBracket));

        $semantics->endStringList();
    }

    protected function string_($parent_id, &$semantics)
    {
        $token = $this->scanner_->nextToken();
        $semantics->validateToken($token);
        $this->tree_->addChildTo($parent_id, $token);
    }

    protected function testlist_($parent_id, &$semantics)
    {
        if (!$this->scanner_->nextTokenIs(SieveToken::LeftParenthesis))
        {
            $this->test_($parent_id, $semantics);
            return;
        }

        $token = $this->scanner_->nextToken();
        $semantics->validateToken($token);
        $this->tree_->addChildTo($parent_id, $token);

        do
        {
            $this->test_($parent_id, $semantics);

            $token = $this->scanner_->nextToken();
            if (!$token->is(SieveToken::Comma|SieveToken::RightParenthesis))
            {
                throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightParenthesis));
            }
            $this->tree_->addChildTo($parent_id, $token);
        }
        while (!$token->is(SieveToken::RightParenthesis));
    }

    protected function test_($parent_id, &$semantics)
    {
        // Check if semantics allow an identifier
        $token = $this->scanner_->nextToken();
        $semantics->validateToken($token);

        // Get semantics for this test command
        $this_semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
        $this_node = $this->tree_->addChildTo($parent_id, $token);

        // Consume eventual argument tokens
        $this->arguments_($this_node, $this_semantics);

        // Check that all required arguments were there
        $token = $this->scanner_->peekNextToken();
        $this_semantics->done($token);
    }

    protected function block_($parent_id, &$semantics)
    {
        $this->commands_($parent_id, $semantics);

        $token = $this->scanner_->nextToken();
        if (!$token->is(SieveToken::BlockEnd))
        {
            throw new SieveException($token, SieveToken::BlockEnd);
        }
        $this->tree_->addChildTo($parent_id, $token);
    }
}