<?php
/**
 * This file is part of the Peast package
 *
 * (c) Marco Marchiò <marco.mm89@gmail.com>
 *
 * For the full copyright and license information refer to the LICENSE file
 * distributed with this source code
 */
namespace Peast;

use Peast\Syntax\Node\Comment;

/**
 * Nodes renderer class
 * 
 * @author Marco Marchiò <marco.mm89@gmail.com>
 */
class Renderer
{
    /**
     * Formatter to use for the rendering
     * 
     * @var Formatter\Base
     */
    protected $formatter;
    
    /**
     * Rendering options taken from the formatter
     * 
     * @var object
     */
    protected $renderOpts;
    
    /**
     * Node types that does not require semicolon insertion
     * 
     * @var array
     */
    protected $noSemicolon = array(
        "ClassDeclaration",
        "ExportDefaultDeclaration",
        "ForInStatement",
        "ForOfStatement",
        "ForStatement",
        "FunctionDeclaration",
        "IfStatement",
        "LabeledStatement",
        "StaticBlock",
        "SwitchStatement",
        "TryStatement",
        "WhileStatement",
        "WithStatement",
        "MethodDefinition",
        "BlockStatement"
    );
    
    /**
     * Sets the formatter to use for the rendering
     * 
     * @param Formatter\Base    $formatter  Formatter
     * 
     * @return $this
     */
    public function setFormatter(Formatter\Base $formatter)
    {
        $this->formatter = $formatter;
        
        $this->renderOpts = (object) array(
            "nl" => $this->formatter->getNewLine(),
            "ind" => $this->formatter->getIndentation(),
            "nlbc" => $this->formatter->getNewLineBeforeCurlyBracket(),
            "sao" =>  $this->formatter->getSpacesAroundOperator() ? " " : "",
            "sirb" => $this->formatter->getSpacesInsideRoundBrackets() ? " " : "",
            "awb" => $this->formatter->getAlwaysWrapBlocks(),
            "com" => $this->formatter->getRenderComments(),
            "rci" => $this->formatter->getRecalcCommentsIndent()
        );
        
        return $this;
    }
    
    /**
     * Returns the formatter to use for the rendering
     * 
     * @return Formatter\Base
     */
    public function getFormatter()
    {
        return $this->formatter;
    }
    
    /**
     * Renders the given node
     * 
     * @param Syntax\Node\Node  $node   Node to render
     * 
     * @return string
     * 
     * @throws \Exception
     */
    public function render(Syntax\Node\Node $node)
    {
        //Throw exception if no formatter has been specified
        if (!$this->formatter) {
            throw new \Exception("Formatter not set");
        }
        
        //Reset indentation level
        $this->renderOpts->indLevel = 0;
        
        //Start rendering
        return $this->renderNode($node);
    }
    
    /**
     * Renders a node
     * 
     * @param Syntax\Node\Node  $node           Node to render
     * @param bool              $addSemicolon   True to add semicolon after node
     *                                          rendered code
     * 
     * @return string
     */
    protected function renderNode(Syntax\Node\Node $node, $addSemicolon = false)
    {
        $code = "";
        if ($this->renderOpts->com) {
            $code .= $this->renderComments($node);
        }
        $type = $node->getType();
        switch ($type) {
            case "ArrayExpression":
            case "ArrayPattern":
                $code .= "[" .
                         $this->joinNodes(
                            $node->getElements(),
                            "," . $this->renderOpts->sao
                         ) .
                         "]";
            break;
            case "ArrowFunctionExpression":
                if ($node->getAsync()) {
                    $code .= "async" . $this->renderOpts->sao;
                }
                $code .= "(" .
                         $this->renderOpts->sirb .
                         $this->joinNodes(
                            $node->getParams(),
                            "," . $this->renderOpts->sao
                         ) .
                         $this->renderOpts->sirb .
                         ")" .
                         $this->renderOpts->sao .
                         "=>";
                $body = $node->getBody();
                if ($body->getType() !== "BlockStatement") {
                    $code .= $this->renderOpts->sao . $this->renderNode($body);
                } else {
                    $code .= $this->renderStatementBlock($node, $body, true);
                }
            break;
            case "AwaitExpression":
                $code .= "await " . $this->renderNode($node->getArgument());
            break;
            case "AssignmentExpression":
            case "AssignmentPattern":
            case "BinaryExpression":
            case "LogicalExpression":
                $operator = $type === "AssignmentPattern" ?
                            "=" :
                            $node->getOperator();
                $code .= $this->renderNode($node->getLeft());
                $codeRight = $this->renderNode($node->getRight());
                if (preg_match("#^[a-z]+$#i", $operator)) {
                    $code .= " " .
                             $operator .
                             " ";
                } else {
                    //If there's no space around the operator, additional checks must
                    //be performed to prevent errors when rendering unary and update
                    //expressions inside binary expressions
                    $checkSpace = !$this->renderOpts->sao && $type === "BinaryExpression";
                    
                    //The space is mandatory if the left part ends with the same
                    //character used as operator
                    if ($checkSpace && $code && substr($code, -1) === $operator) {
                        $code .= " ";
                    }
                    
                    $code .= $this->renderOpts->sao .
                             $operator .
                             $this->renderOpts->sao;
                    
                    //The space is mandatory if the right part begins with the same
                    //character used as operator
                    if ($checkSpace && $codeRight && $codeRight[0] === $operator) {
                        $code .= " ";
                    }
                }
                $code .= $codeRight;
            break;
            case "BlockStatement":
                $code .= trim($this->renderStatementBlock(
                    $node, $node->getBody(), true, false, true, true
                ));
            break;
            case "ClassBody":
            case "Program":
                $code .= $this->renderStatementBlock(
                    $node, $node->getBody(), false, false, true, false
                );
            break;
            case "BreakStatement":
            case "ContinueStatement":
                $code .= $type === "BreakStatement" ? "break" : "continue";
                if ($label = $node->getLabel()) {
                    $code .= " " . $this->renderNode($label);
                }
            break;
            case "CallExpression":
            case "NewExpression":
                if ($type === "NewExpression") {
                    $code .= "new ";
                    $optional = false;
                } else {
                    $optional = $node->getOptional();
                }
                $code .= $this->renderNode($node->getCallee()) .
                         ($optional ? "?." : "") .
                         "(" .
                         $this->renderOpts->sirb .
                         $this->joinNodes(
                            $node->getArguments(),
                            "," . $this->renderOpts->sao
                         ) .
                         $this->renderOpts->sirb .
                         ")";
            break;
            case "CatchClause":
                $code .= "catch";
                if ($params = $node->getParam()) {
                    $code .= $this->renderOpts->sao .
                             "(" .
                             $this->renderOpts->sirb .
                             $this->renderNode($params) .
                             $this->renderOpts->sirb .
                             ")";
                }
                $code .= $this->renderStatementBlock($node, $node->getBody(), true);
            break;
            case "ChainExpression":
            case "ExpressionStatement":
                $code .= $this->renderNode($node->getExpression());
            break;
            case "ClassExpression":
            case "ClassDeclaration":
                $code .= "class";
                if ($id = $node->getId()) {
                    $code .= " " . $this->renderNode($id);
                }
                if ($superClass = $node->getSuperClass()) {
                    $code .= " extends " . $this->renderNode($superClass);
                }
                $code .= $this->renderStatementBlock(
                    $node, $node->getBody(), true
                );
            break;
            case "ConditionalExpression":
                $code .= $this->renderNode($node->getTest()) .
                         $this->renderOpts->sao .
                         "?" .
                         $this->renderOpts->sao .
                         $this->renderNode($node->getConsequent()) .
                         $this->renderOpts->sao .
                         ":" .
                         $this->renderOpts->sao .
                         $this->renderNode($node->getAlternate());
            break;
            case "DebuggerStatement":
                $code .= "debugger";
            break;
            case "DoWhileStatement":
                $code .= "do" .
                         $this->renderStatementBlock(
                            $node, $node->getBody(), null, true
                         ) .
                         $this->renderOpts->sao .
                         "while" .
                         $this->renderOpts->sao .
                         "(" .
                         $this->renderOpts->sirb .
                         $this->renderNode($node->getTest()) .
                         $this->renderOpts->sirb .
                         ")";
            break;
            case "JSXEmptyExpression":
            case "EmptyStatement":
            break;
            case "ExportAllDeclaration":
                $code .= "export *";
                $exported = $node->getExported();
                if ($exported) {
                    $code .= " as " . $this->renderNode($exported);
                }
                $code .= " from " . $this->renderNode($node->getSource());
                $code .= $this->renderImportAttributes($node);
            break;
            case "ExportDefaultDeclaration":
                $declaration = $node->getDeclaration();
                $code .= "export default " .
                         $this->renderNode($declaration);
                if ($this->requiresSemicolon($declaration)) {
                    $code .= ";";
                }
            break;
            case "ExportNamedDeclaration":
                $code .= "export";
                if ($dec = $node->getDeclaration()) {
                    $code .= " " .
                             $this->renderNode($dec);
                } else {
                    $code .= $this->renderOpts->sao .
                             "{" .
                             $this->joinNodes(
                                 $node->getSpecifiers(),
                                 "," . $this->renderOpts->sao
                             ) .
                             "}";
                    if ($source = $node->getSource()) {
                        $code .= $this->renderOpts->sao .
                                 "from " .
                                 $this->renderNode($source);
                    }
                    $code .= $this->renderImportAttributes($node);
                }
            break;
            case "ExportSpecifier":
                $local = $this->renderNode($node->getLocal());
                $ref = $this->renderNode($node->getExported());
                $code .= $local === $ref ?
                         $local :
                         $local . " as " . $ref;
            break;
            case "ForInStatement":
            case "ForOfStatement":
                //Force single line mode for substatements
                $this->renderOpts->forceSingleLine = true;
                $code .= "for" .
                         ($type === "ForOfStatement" && $node->getAwait() ? " await" : "") .
                         $this->renderOpts->sao .
                         "(" .
                         $this->renderOpts->sirb .
                         $this->renderNode($node->getLeft()) .
                         " " . ($type === "ForInStatement" ? "in" : "of") . " " .
                         $this->renderNode($node->getRight()) .
                         $this->renderOpts->sirb .
                         ")" .
                         $this->renderStatementBlock($node, $node->getBody());
                unset($this->renderOpts->forceSingleLine);
            break;
            case "ForStatement":
                //Force single line mode for substatements
                $this->renderOpts->forceSingleLine = true;
                $code .= "for" .
                         $this->renderOpts->sao .
                         "(" .
                         $this->renderOpts->sirb;
                if ($init = $node->getInit()) {
                    $code .= $this->renderNode($init);
                }
                $code .= ";" . $this->renderOpts->sao;
                if ($test = $node->getTest()) {
                    $code .= $this->renderNode($test);
                }
                $code .= ";" . $this->renderOpts->sao;
                if ($update = $node->getUpdate()) {
                    $code .= $this->renderNode($update);
                }
                $code .= $this->renderOpts->sirb .
                         ")" .
                         $this->renderStatementBlock($node, $node->getBody());
                unset($this->renderOpts->forceSingleLine);
            break;
            case "FunctionDeclaration":
            case "FunctionExpression":
                $id = $node->getId();
                if ($node->getAsync()) {
                    $code .= "async ";
                }
                $code .= "function";
                if ($node->getGenerator()) {
                    $code .= $this->renderOpts->sao .
                             "*";
                } elseif ($id) {
                    $code .= " ";
                }
                if ($id) {
                    if ($node->getGenerator()) {
                        $code .= $this->renderOpts->sao;
                    }
                    $code .= $this->renderNode($id);
                }
                $code .= $this->renderOpts->sao .
                         "(" .
                         $this->renderOpts->sirb .
                         $this->joinNodes(
                            $node->getParams(),
                            "," . $this->renderOpts->sao
                         ) .
                         $this->renderOpts->sirb .
                         ")" .
                         $this->renderStatementBlock($node, $node->getBody(), true);
            break;
            case "ImportExpression":
                $options = $node->getOptions();
                $code .= "import(" .
                         $this->renderOpts->sirb .
                         $this->renderNode($node->getSource()) .
                         (
                            $options ?
                            "," . $this->renderOpts->sao . $this->renderNode($options) :
                            ""
                         ) .
                         $this->renderOpts->sirb .
                         ")";
            break;
            case "JSXIdentifier":
            case "Identifier":
                $code .= $node->getRawName();
            break;
            case "IfStatement":
                $code .= "if" .
                         $this->renderOpts->sao .
                         "(" .
                         $this->renderOpts->sirb .
                         $this->renderNode($node->getTest()) .
                         $this->renderOpts->sirb .
                         ")";
                
                $code .= $this->renderStatementBlock(
                    $node, $node->getConsequent()
                );
                if ($alternate = $node->getAlternate()) {
                    $code .= $this->renderOpts->sao .
                             "else" .
                             $this->renderStatementBlock(
                                 $node,
                                 $alternate,
                                 null,
                                 true
                             );
                }
            break;
            case "ImportAttribute":
                $code .= $this->renderNode($node->getKey()) .
                         ":" .
                         $this->renderOpts->sao .
                         $this->renderNode($node->getValue());
            break;
            case "ImportDeclaration":
                $code .= "import ";
                $specifiers = $node->getSpecifiers();
                if (count($specifiers)) {
                    $sep = "," . $this->renderOpts->sao;
                    $groups = $parts = array();
                    foreach ($specifiers as $spec) {
                        $specType = $spec->getType();
                        if (!isset($groups[$specType])) {
                            $groups[$specType] = array();
                        }
                        $groups[$specType][] = $spec;
                    }
                    if (isset($groups["ImportDefaultSpecifier"])) {
                        foreach ($groups["ImportDefaultSpecifier"] as $s) {
                            $parts[] = $this->renderNode($s);
                        }
                    }
                    if (isset($groups["ImportNamespaceSpecifier"])) {
                        foreach ($groups["ImportNamespaceSpecifier"] as $s) {
                            $parts[] = $this->renderNode($s);
                        }
                    }
                    if (isset($groups["ImportSpecifier"])) {
                        $impSpec = array();
                        foreach ($groups["ImportSpecifier"] as $s) {
                            $impSpec[] = $this->renderNode($s);
                        }
                        $parts[] = "{" . implode($sep, $impSpec) . "}";
                    }
                    $code .= implode($sep, $parts) . " from ";
                }
                $code .= $this->renderNode($node->getSource());
                $code .= $this->renderImportAttributes($node);
            break;
            case "ImportDefaultSpecifier":
                $code .= $this->renderNode($node->getLocal());
            break;
            case "ImportNamespaceSpecifier":
                $code .= "* as " . $this->renderNode($node->getLocal());
            break;
            case "ImportSpecifier":
                $local = $this->renderNode($node->getLocal());
                $ref = $this->renderNode($node->getImported());
                $code .= $local === $ref ?
                         $local :
                         $ref . " as " . $local;
            break;
            case "JSXAttribute":
                $code .= $this->renderNode($node->getName());
                if ($value = $node->getValue()) {
                    $code .= "=" . $this->renderNode($value);
                }
            break;
            case "JSXClosingElement":
                $code .= "</" . $this->renderNode($node->getName()) . ">";
            break;
            case "JSXClosingFragment":
                $code .= "</>";
            break;
            case "JSXElement":
                $code .= $this->renderNode($node->getOpeningElement()) .
                         $this->joinNodes($node->getChildren(), "");
                if ($closing = $node->getClosingElement()) {
                    $code .= $this->renderNode($closing);
                }
            break;
            case "JSXExpressionContainer":
                $code .= "{" . $this->renderNode($node->getExpression()) . "}";
            break;
            case "JSXFragment":
                $code .= $this->renderNode($node->getOpeningFragment()) .
                         $this->joinNodes($node->getChildren(), "") .
                         $this->renderNode($node->getClosingFragment());
            break;
            case "JSXNamespacedName":
                $code .= $this->renderNode($node->getNamespace()) . ":" .
                         $this->renderNode($node->getName());
            break;
            case "JSXOpeningElement":
                $code .= "<" . $this->renderNode($node->getName());
                $attributes = $node->getAttributes();
                if (count($attributes)) {
                    $code .= " " . $this->joinNodes($attributes, " ");
                }
                if ($node->getSelfClosing()) {
                    $code .= "/";
                }
                $code .= ">";
            break;
            case "JSXOpeningFragment":
                $code .= "<>";
            break;
            case "JSXSpreadAttribute":
                $code .= "{..." . $this->renderNode($node->getArgument()) . "}";
            break;
            case "JSXSpreadChild":
                $code .= "{..." . $this->renderNode($node->getExpression()) . "}";
            break;
            case "LabeledStatement":
                $body = $node->getBody();
                $code .= $this->renderNode($node->getLabel()) .
                         ":";
                if ($body->getType() === "BlockStatement") {
                    $code .= $this->renderStatementBlock($node, $body, true);
                } else {
                    $code .= $this->renderOpts->nl .
                             $this->getIndentation() .
                             $this->renderNode($body);
                }
                if ($this->requiresSemicolon($body)) {
                    $code .= ";";
                }
            break;
            case "JSXText":
            case "Literal":
            case "RegExpLiteral":
                $code .= $node->getRaw();
            break;
            case "JSXMemberExpression":
            case "MemberExpression":
                $property = $node->getProperty();
                $compiledProperty = $this->renderNode($property);
                $code .= $this->renderNode($node->getObject());
                $optional = false;
                if ($type === "MemberExpression") {
                    $optional = $node->getOptional();
                }
                $propertyType = $property->getType();
                if ($type === "MemberExpression" &&
                    ($node->getComputed() ||
                    ($propertyType !== "Identifier" && $propertyType !== "PrivateIdentifier"))) {
                    $code .= ($optional ? "?." : "") . "[" . $compiledProperty . "]";
                } else {
                    $code .= ($optional ? "?." : ".") . $compiledProperty;
                }
            break;
            case "MetaProperty":
                $code .= $node->getMeta() . "." . $node->getProperty();
            break;
            case "MethodDefinition":
                if ($node->getStatic()) {
                    $code .= "static ";
                }
                $value = $node->getValue();
                $key = $node->getKey();
                $kind = $node->getKind();
                if ($kind === $node::KIND_GET || $kind === $node::KIND_SET) {
                    $code .= $kind . " ";
                } else {
                    if ($value->getAsync()) {
                        $code .= "async ";
                    }
                    if ($value->getGenerator()) {
                        $code .= "*" .
                                 $this->renderOpts->sao;
                    }
                }
                if ($node->getComputed()) {
                    $code .= "[" .
                             $this->renderNode($key) .
                             "]";
                } else {
                    $code .= $this->renderNode($key);
                }
                $code .= $this->renderOpts->sao .
                         preg_replace("/^[^(]+/", "", $this->renderNode($value));
            break;
            case "ObjectExpression":
                $currentIndentation = $this->getIndentation();
                $this->renderOpts->indLevel++;
                $indentation = $this->getIndentation();
                //Handle single line mode
                if (isset($this->renderOpts->forceSingleLine)) {
                    $start = $end = "";
                    $separator = "," . $this->renderOpts->sao;
                } else {
                    $end = $this->renderOpts->nl . $currentIndentation;
                    $start = $this->renderOpts->nl . $indentation;
                    $separator = "," . $this->renderOpts->nl . $indentation;
                }
                $code .= "{";
                $properties = $node->getProperties();
                if (count($properties)) {
                    $code .= $start .
                             $this->joinNodes(
                                $properties,
                                $separator
                             ) .
                             $end;
                }
                $code .= "}";
                $this->renderOpts->indLevel--;
            break;
            case "ObjectPattern":
                $code .= "{" .
                         $this->joinNodes(
                            $node->getProperties(),
                            "," . $this->renderOpts->sao
                         ) .
                         "}";
            break;
            case "ParenthesizedExpression":
                $code .= "(" .
                         $this->renderOpts->sirb .
                         $this->renderNode($node->getExpression()) .
                         $this->renderOpts->sirb .
                         ")";
            break;
            case "PrivateIdentifier":
                $code .= "#" . $node->getName();
            break;
            case "Property":
                $value = $node->getValue();
                $key = $node->getKey();
                $compiledKey = $this->renderNode($key);
                $compiledValue = $this->renderNode($value);
                $keyType = $key->getType();
                $valueType = $value->getType();
                if ($valueType === "AssignmentPattern" &&
                    $compiledKey === $this->renderNode($value->getLeft())) {
                    $code .= $compiledValue;
                } else {
                    $kind = $node->getKind();
                    $getterSetter = $kind === $node::KIND_GET ||
                                    $kind === $node::KIND_SET;
                    if ($getterSetter) {
                        $code .= $kind . " ";
                    } elseif ($value->getType() === "FunctionExpression" &&
                              $value->getGenerator()) {
                        $code .= "*" .
                                 $this->renderOpts->sao;
                    }
                    if ($node->getMethod() && $value->getAsync()) {
                        $code .= "async ";
                    }
                    if ($node->getComputed()) {
                        $code .= "[" . $compiledKey . "]";
                    } else {
                        $code .= $compiledKey;
                    }
                    if ($node->getMethod() || $getterSetter) {
                        $code .= $this->renderOpts->sao .
                                 preg_replace("/^[^(]+/", "", $compiledValue);
                    } elseif ($keyType !== "Identifier" ||
                              $valueType !== "Identifier" ||
                              $compiledKey !== $compiledValue
                    ) {
                        $code .= ($node->getShorthand() ? "=" : ":") .
                                 $this->renderOpts->sao .
                                 $compiledValue;
                    }
                }
            break;
            case "PropertyDefinition":
                if ($node->getStatic()) {
                    $code .= "static ";
                }
                $compiledKey = $this->renderNode($node->getKey());
                if ($node->getComputed()) {
                    $code .= "[" . $compiledKey . "]";
                } else {
                    $code .= $compiledKey;
                }
                if ($value = $node->getValue()) {
                    $code .= $this->renderOpts->sao .
                             "=" .
                             $this->renderOpts->sao .
                             $this->renderNode($value);
                }
            break;
            case "RestElement":
            case "SpreadElement":
                $code .= "..." . $this->renderNode($node->getArgument());
            break;
            case "ReturnStatement":
                $code .= "return";
                if ($argument = $node->getArgument()) {
                    $code .= " " . $this->renderNode($argument);
                }
            break;
            case "SequenceExpression":
                $code .= $this->joinNodes(
                            $node->getExpressions(),
                            "," . $this->renderOpts->sao
                         );
            break;
            case "StaticBlock":
                $code .= "static";
                $code .= $this->renderStatementBlock($node, $node->getBody(), true);
            break;
            case "Super":
                $code .= "super";
            break;
            case "SwitchCase":
                if ($test = $node->getTest()) {
                    $code .= "case " . $this->renderNode($test);
                } else {
                    $code .= "default";
                }
                $code .= ":";
                if (count($node->getConsequent())) {
                    $code .= $this->renderStatementBlock(
                                 $node,
                                 $node->getConsequent()
                             );
                }
            break;
            case "SwitchStatement":
                $code .= "switch" .
                         $this->renderOpts->sao .
                         "(" .
                         $this->renderOpts->sirb .
                         $this->renderNode($node->getDiscriminant()) .
                         $this->renderOpts->sirb .
                         ")" .
                         $this->renderStatementBlock(
                             $node, $node->getCases(), true, false, false
                         );
            break;
            case "TaggedTemplateExpression":
                $code .= $this->renderNode($node->getTag()) .
                         $this->renderNode($node->getQuasi());
            break;
            case "TemplateElement":
                $code .= $node->getRawValue();
            break;
            case "TemplateLiteral":
                $code .= "`";
                foreach ($node->getParts() as $part) {
                    if ($part->getType() === "TemplateElement") {
                        $code .= $this->renderNode($part);
                    } else {
                        $code .= "$" . "{" . $this->renderNode($part) . "}";
                    }
                }
                $code .= "`";
            break;
            case "ThisExpression":
                $code .= "this";
            break;
            case "ThrowStatement":
                $code .= "throw " . $this->renderNode($node->getArgument());
            break;
            case "TryStatement":
                $code .= "try" .
                         $this->renderStatementBlock($node, $node->getBlock(), true);
                if ($handler = $node->getHandler()) {
                    $code .= $this->renderOpts->sao .
                             $this->renderNode($handler);
                }
                if ($finalizer = $node->getFinalizer()) {
                    $code .= $this->renderOpts->sao .
                             "finally" .
                             $this->renderStatementBlock($node, $finalizer, true);
                }
            break;
            case "UnaryExpression":
            case "UpdateExpression":
                $prefix = $node->getPrefix();
                if ($prefix) {
                    $code .= $node->getOperator();
                    if (preg_match("#^[a-z]+$#i", $node->getOperator())) {
                        $code .= " ";
                    }
                }
                $code .= $this->renderNode($node->getArgument());
                if (!$prefix) {
                    $code .= $node->getOperator();
                }
            break;
            case "VariableDeclaration":
                $this->renderOpts->indLevel++;
                $indentation = $this->getIndentation();
                //Handle single line mode
                if (isset($this->renderOpts->forceSingleLine)) {
                    $separator = "," . $this->renderOpts->sao;
                } else {
                    $separator = "," . $this->renderOpts->nl . $indentation;
                }
                $code .= $node->getKind() .
                         " " .
                         $this->joinNodes(
                            $node->getDeclarations(),
                            $separator
                         );
                $this->renderOpts->indLevel--;
            break;
            case "VariableDeclarator":
                $code .= $this->renderNode($node->getId());
                if ($init = $node->getInit()) {
                    $code .= $this->renderOpts->sao .
                             "=" .
                             $this->renderOpts->sao .
                             $this->renderNode($init);
                }
            break;
            case "WhileStatement":
                $code .= "while" .
                         $this->renderOpts->sao .
                         "(" .
                         $this->renderOpts->sirb .
                         $this->renderNode($node->getTest()) .
                         $this->renderOpts->sirb .
                         ")" .
                         $this->renderStatementBlock($node, $node->getBody());
            break;
            case "WithStatement":
                $code .= "with" .
                         $this->renderOpts->sao .
                         "(" .
                         $this->renderOpts->sirb .
                         $this->renderNode($node->getObject()) .
                         $this->renderOpts->sirb .
                         ")" .
                         $this->renderStatementBlock($node, $node->getBody());
            break;
            case "YieldExpression":
                $code .= "yield";
                if ($node->getDelegate()) {
                    $code .= " *";
                }
                if ($argument = $node->getArgument()) {
                    $code .= " " . $this->renderNode($argument);
                }
            break;
        }
        if ($addSemicolon) {
            $code .= ";";
        }
        if ($this->renderOpts->com) {
            $code .= $this->renderComments($node, false);
        }
        return $code;
    }
    
    /**
     * Renders a node as a block statement
     *
     * @param Syntax\Node\Node          $parent             Parent node
     * @param Syntax\Node\Node|array    $node               Node or array of 
     *                                                      nodes to render
     * @param bool                      $forceBrackets      Overrides brackets
     *                                                      inserting rules
     * @param bool                      $mandatorySeparator True if a starting
     *                                                      separator is
     *                                                      mandatory
     * @param bool                      $addSemicolons      Semicolons are
     *                                                      inserted automatically
     *                                                      if this parameter is
     *                                                      not false
     * @param bool                      $incIndent          If false indentation
     *                                                      level won't be
     *                                                      incremented
     * 
     * @return string
     */
    protected function renderStatementBlock(
        $parent, $node, $forceBrackets = null, $mandatorySeparator = false,
        $addSemicolons = true, $incIndent = true
    ) {
        $code = "";
        
        //If node is an array with only one element, handle it as a single node
        if (is_array($node) && count($node) === 1) {
            $node = $node[0];
        }

        //Special handling of BlockStatement and ClassBody nodes by rendering
        //their child nodes
        $origNode = null;
        if (!is_array($node) &&
            in_array($node->getType(), array("BlockStatement", "ClassBody"))) {
            $origNode = $node;
            $node = $node->getBody();
        }
        
        //If $forceBrackets is not null use its value to override curly brackets
        //insertion rules
        if ($forceBrackets !== null) {
            $hasBrackets = $forceBrackets;
        } else {
            //Insert curly brackets if needed
            $hasBrackets = $this->needsBrackets($parent, $node);
        }
        $currentIndentation = $this->getIndentation();
        
        //If the node must be wrapped in curly braces a separator defined by formatter
        //must be inserted
        if ($hasBrackets) {
            if ($this->renderOpts->nlbc) {
                $code .= $this->renderOpts->nl . $currentIndentation;
            } else {
                $code .= $this->renderOpts->sao;
            }
        }
        //Move the content of the SwitchCase content to a new line if brackets won't
        //be inserted, in this way it's not rendered on the same line of the "case"
        elseif ($parent->getType() === "SwitchCase") {
            $code .= $this->renderOpts->nl;
        }

        $emptyBody = is_array($node) && !count($node);

        if ($this->renderOpts->com && $origNode) {
            $code .= $this->renderComments($origNode, true, !$emptyBody);
        }
        
        //Insert open curly bracket if required
        if ($hasBrackets) {
            $code .= "{" . $this->renderOpts->nl;
        } elseif ($mandatorySeparator) {
            //If bracket is not inserted but a separator is still required
            //a space is added
            $code .= " ";
        }
        
        //Increase indentation level
        if ($incIndent) {
            $this->renderOpts->indLevel++;
        }
        $subIndentation = $this->getIndentation();
        
        //Render the node or the array of nodes
        if (is_array($node)) {
            if (!$emptyBody) {
                $code .= $subIndentation .
                         $this->joinNodes(
                            $node,
                            $this->renderOpts->nl . $subIndentation,
                            $addSemicolons
                         );
            }
        } else {
            $code .= $subIndentation . $this->renderNode(
                $node,
                $addSemicolons && $this->requiresSemicolon($node)
            );
        }
        
        if ($this->renderOpts->com) {
            //Strip last new line and indentations added by comments rendering
            if (!$emptyBody) {
                $code = $this->trimEmptyLine($code);
            }
            if ($origNode) {
                $code .= $this->renderComments($origNode, false, !$emptyBody);
                if (!$emptyBody) {
                    $code = $this->trimEmptyLine($code);
                }
            }
        }
        
        //Reset the indentation level
        if ($incIndent) {
            $this->renderOpts->indLevel--;
        }
        
        //Insert closing curly bracket if required
        if ($hasBrackets) {
            //Add a new line if something was rendered
            if (!$emptyBody) {
                $code .= $this->renderOpts->nl;
            }
            $code .= $currentIndentation . "}";
        }
        
        return $code;
    }
    
    /**
     * Joins an array of nodes with the given separator
     * 
     * @param array     $nodes          Nodes
     * @param string    $separator      Separator
     * @param bool      $addSemicolons  True to add semicolons after each node
     * 
     * @return string
     */
    protected function joinNodes($nodes, $separator, $addSemicolons=false)
    {
        $parts = array();
        foreach ($nodes as $node) {
            if (!$node) {
                $code = "";
            } else {
                $code = $this->renderNode(
                    $node,
                    $addSemicolons && $this->requiresSemicolon($node)
                );
            }
            $parts[] = $code;
        }
        return implode($separator, $parts);
    }
    
    /**
     * Check if the node or the array of nodes need brackets to be rendered
     *
     * @param Syntax\Node\Node          $parent             Parent node
     * @param Syntax\Node\Node|array    $node               Node or array of
     *                                                      nodes to render
     * 
     * @return bool
     */
    protected function needsBrackets($parent, $node)
    {
        $parentType = $parent->getType();
        //The SwitchCase content needs brackets if it contains let or const declarations
        $inSwitchCase = $parentType === "SwitchCase";
        //Except for SwitchCase, brackets are needed if the formatter requires them or whenever
        //there are 0 or multiple nodes to render
        if (!$inSwitchCase && ($this->renderOpts->awb || (is_array($node) && count($node) !== 1))) {
            return true;
        }
        if (!is_array($node)) {
            $node = array($node);
        }
        $addBrackets = false;
        $optBracketNodes = array(
            "DoWhileStatement", "ForInStatement", "ForOfStatement",
            "ForStatement", "WhileStatement", "WithStatement"
        );
        //An IfStatement requires brackets if it contains let or const declarations and if it has the "else" part
        //and contains another IfStatement with "else"
        $inIfWithElse = $parentType === "IfStatement" && $parent->getAlternate();
        $checkFn = function ($n) use ($inIfWithElse, $optBracketNodes, &$addBrackets) {
            $type = $n->getType();
            if ($inIfWithElse && $type === "IfStatement") {
                if (!$n->getAlternate()) {
                    $addBrackets = true;
                }
                return Traverser::DONT_TRAVERSE_CHILD_NODES;
            } elseif ($type === "BlockStatement") {
                if (count($n->getBody()) !== 1) {
                    return Traverser::DONT_TRAVERSE_CHILD_NODES;
                }
            } elseif ($inIfWithElse && $type === "LabeledStatement") {
                if ($n->getBody()->getType() === "BlockStatement") {
                    $addBrackets = true;
                }
                return Traverser::DONT_TRAVERSE_CHILD_NODES;
            } elseif ($type === "VariableDeclaration") {
                if (in_array($n->getKind(), array($n::KIND_LET, $n::KIND_CONST))) {
                    $addBrackets = true;
                }
                return Traverser::DONT_TRAVERSE_CHILD_NODES;
            } elseif (!in_array($type, $optBracketNodes)) {
                return Traverser::DONT_TRAVERSE_CHILD_NODES;
            }
        };
        //Traverse every node but stop whenever the $addBrackets variable becomes true
        foreach ($node as $n) {
            $n->traverse($checkFn);
            if ($addBrackets) {
                break;
            }
        }
        return $addBrackets;
    }

    /**
     * Render node's comments
     * 
     * @param Syntax\Node\Node  $node             Node
     * @param bool              $leading          False to render trailing comments
     * @param bool|null         $blockContent     This parameter can have 3 values:
     *                                            - null: the node is not a block
     *                                            - false: the node is an empty block
     *                                            - true: the node is a block with content
     * 
     * @return string
     */
    protected function renderComments($node, $leading = true, $blockContent = null)
    {
        $code = "";
        $fn = $leading ? "getLeadingComments" : "getTrailingComments";
        $comments = $node ? $node->$fn() : array();
        $numComments = count($comments);
        if ($numComments) {
            $lastFormatted = $blockContent === false ? true : $leading;
            $refNode = $node;
            $refKey = $leading ? "end" : "start";
            $refNodeKey = $leading ? "start" : "end";
            $indent = $this->getIndentation();
            foreach ($comments as $k => $comment) {                
                $lastComment = $k === $numComments - 1;
                $isMultilineComment = $comment->getKind() === Comment::KIND_MULTILINE;
                // Check if the comment must be formatted with new line and indentations
                $format = true;
                if (
                    $refNode && $isMultilineComment &&
                    $comment->location && $refNode->location &&
                    $comment->location->$refKey->getLine() === $refNode->location->$refNodeKey->getLine()
                ) {
                    $format = false;
                }
                //If the last comment wasn't formatted but this one must be formatted, add the new
                //line and the indentation
                if ($format && !$lastFormatted) {
                    $code .= $this->renderOpts->nl . $indent;
                }
                //Leading comments on empty blocks must render the initial indentation if format
                //is enabled
                if ($format && $blockContent === false && !$k) {
                    $code .= $indent;
                }

                $commentRaw = $comment->getRawText();

                //Reindent multiline comments if necessary
                if ($isMultilineComment && $indent && $this->renderOpts->rci) {
                    $commentRaw = preg_replace("/^\s+\*/m", $indent . " *", $commentRaw);
                }

                $code .= $commentRaw;

                //If format is enabled, add the new line character and the indentation if the node
                //is not an empty block or the it's not the last comment
                if ($format && ($blockContent !== true || !$lastComment || !$isMultilineComment)) {
                    //For non multiline comments the new line is mandatory, even if the formatter
                    //disables it
                    $code .= !$isMultilineComment && !$this->renderOpts->nl ? "\n" : $this->renderOpts->nl;
                    //Last comment on blocks must not render indentation
                    if ($blockContent === null || !$lastComment) {
                        $code .= $indent;
                    }
                }
                $refNode = $comment;
                $lastFormatted = $format;
            }
        }
        return $code;
    }

    /**
     * Render import attributes of the given node
     * 
     * @param Syntax\Node\Node  $node Node
     * 
     * @return string
     */
    protected function renderImportAttributes($node)
    {
        $code = "";
        if (count($node->getAttributes())) {
            $code .= " with" .
                     $this->renderOpts->sao .
                     "{" .
                     $this->joinNodes(
                        $node->getAttributes(),
                        "," . $this->renderOpts->sao
                     ) .
                     "}";
        }
        return $code;
    }

    /**
     * Removes an empty line at the end of the given code, if present
     * 
     * @param string  $code   Code
     * 
     * @return string
     */
    protected function trimEmptyLine($code)
    {
        if ($this->renderOpts->nl) {
            $nl = preg_quote($this->renderOpts->nl, "/");
            $indent = preg_quote($this->getIndentation(), "/");
            $code = preg_replace("/$nl(?:$indent)?$/", "", $code);
        }
        return $code;
    }
    
    /**
     * Check if the given node requires semicolons insertion
     * 
     * @param Syntax\Node\Node  $node   Node
     * 
     * @return bool
     */
    protected function requiresSemicolon($node)
    {
        return !in_array($node->getType(), $this->noSemicolon);
    }
    
    /**
     * Returns the current indentation string
     * 
     * @return string
     */
    protected function getIndentation()
    {
        return str_repeat(
            $this->renderOpts->ind,
            $this->renderOpts->indLevel
        );
    }
}