/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package groovy.console.ui

import groovy.transform.AutoFinal
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.apache.groovy.io.StringBuilderWriter
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.ConstructorNode
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.GenericsType
import org.codehaus.groovy.ast.GroovyClassVisitor
import org.codehaus.groovy.ast.GroovyCodeVisitor
import org.codehaus.groovy.ast.ImportNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.PackageNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.PropertyNode
import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.ArrayExpression
import org.codehaus.groovy.ast.expr.AttributeExpression
import org.codehaus.groovy.ast.expr.BinaryExpression
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression
import org.codehaus.groovy.ast.expr.BooleanExpression
import org.codehaus.groovy.ast.expr.CastExpression
import org.codehaus.groovy.ast.expr.ClassExpression
import org.codehaus.groovy.ast.expr.ClosureExpression
import org.codehaus.groovy.ast.expr.ClosureListExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.ConstructorCallExpression
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.expr.ElvisOperatorExpression
import org.codehaus.groovy.ast.expr.EmptyExpression
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.expr.FieldExpression
import org.codehaus.groovy.ast.expr.GStringExpression
import org.codehaus.groovy.ast.expr.LambdaExpression
import org.codehaus.groovy.ast.expr.ListExpression
import org.codehaus.groovy.ast.expr.MapEntryExpression
import org.codehaus.groovy.ast.expr.MapExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.MethodPointerExpression
import org.codehaus.groovy.ast.expr.MethodReferenceExpression
import org.codehaus.groovy.ast.expr.NotExpression
import org.codehaus.groovy.ast.expr.PostfixExpression
import org.codehaus.groovy.ast.expr.PrefixExpression
import org.codehaus.groovy.ast.expr.PropertyExpression
import org.codehaus.groovy.ast.expr.RangeExpression
import org.codehaus.groovy.ast.expr.SpreadExpression
import org.codehaus.groovy.ast.expr.SpreadMapExpression
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
import org.codehaus.groovy.ast.expr.SwitchExpression
import org.codehaus.groovy.ast.expr.TernaryExpression
import org.codehaus.groovy.ast.expr.TupleExpression
import org.codehaus.groovy.ast.expr.UnaryMinusExpression
import org.codehaus.groovy.ast.expr.UnaryPlusExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.stmt.AssertStatement
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.ast.stmt.BreakStatement
import org.codehaus.groovy.ast.stmt.CaseStatement
import org.codehaus.groovy.ast.stmt.CatchStatement
import org.codehaus.groovy.ast.stmt.ContinueStatement
import org.codehaus.groovy.ast.stmt.DoWhileStatement
import org.codehaus.groovy.ast.stmt.EmptyStatement
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.ast.stmt.ForStatement
import org.codehaus.groovy.ast.stmt.IfStatement
import org.codehaus.groovy.ast.stmt.ReturnStatement
import org.codehaus.groovy.ast.stmt.Statement
import org.codehaus.groovy.ast.stmt.SwitchStatement
import org.codehaus.groovy.ast.stmt.SynchronizedStatement
import org.codehaus.groovy.ast.stmt.ThrowStatement
import org.codehaus.groovy.ast.stmt.TryCatchStatement
import org.codehaus.groovy.ast.stmt.WhileStatement
import org.codehaus.groovy.ast.stmt.YieldStatement
import org.codehaus.groovy.classgen.BytecodeExpression
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.classgen.Verifier
import org.codehaus.groovy.classgen.asm.ClosureWriter
import org.codehaus.groovy.control.CompilationFailedException
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.syntax.Types

import java.lang.reflect.Modifier
import java.security.CodeSource

/**
 * This class takes Groovy source code, compiles it to a specific compile phase, and then decompiles it
 * back to the groovy source. It is used by GroovyConsole's AST Browser, but can also be invoked from
 * the command line.
 */
@AutoFinal @CompileStatic
class AstNodeToScriptAdapter {

    /**
     * Run this class as a script to compile a groovy file and print out the resulting source.
     * @param args
     *      a filename to compile and a CompilePhase to run to
     */
    static void main(String[] args) {

        if (!args || args.length < 2) {
            println '''
Usage: java groovy.inspect.swingui.AstNodeToScriptAdapter [filename] [compilephase]
where [filename] is a Groovy script
and [compilephase] is a valid Integer based org.codehaus.groovy.control.CompilePhase'''
        } else {
            def file = new File((String) args[0])
            def phase = CompilePhase.fromPhaseNumber(args[1] as int)
            if (!file.exists()) {
                println "File ${args[0]} cannot be found."
            } else if (phase == null) {
                println "Compile phase ${args[1]} cannot be mapped to a org.codehaus.groovy.control.CompilePhase."
            } else {
                println new AstNodeToScriptAdapter().compileToScript(file.text, phase.getPhaseNumber())
            }
        }
    }

    /**
     * This method takes source code, compiles it, then reverses it back to source.
     *
     * @param script
     *    the source code to be compiled. If invalid, a compile error occurs
     * @param compilePhase
     *    the CompilePhase. Must be an int mapped in {@link CompilePhase}
     * @param classLoader
     *    (optional) the classloader to use. If missing/null then the current is used.
     *    This parameter enables things like ASTBrowser to invoke this with the correct classpath
     * @param showScriptFreeForm
     *    Whether or not to show the script portion of the source code
     * @param showScriptClass
     *    Whether or not to show the Script class from the source code
     * @param config
     *    optional compiler configuration
     * @returns the source code from the AST state
     */
    String compileToScript(String script, int compilePhase, ClassLoader classLoader = null, boolean showScriptFreeForm = true, boolean showScriptClass = true, CompilerConfiguration config = null) {
        def writer = new StringBuilderWriter()
        def scriptName = 'script' + System.currentTimeMillis() + '.groovy'
        GroovyCodeSource codeSource = new GroovyCodeSource(script, scriptName, '/groovy/script')
        boolean ownLoader = classLoader == null
        GroovyClassLoader loader = (GroovyClassLoader) classLoader ?: new GroovyClassLoader(this.class.classLoader)
        try {
            CompilationUnit cu = new CompilationUnit((CompilerConfiguration) (config ?: CompilerConfiguration.DEFAULT), (CodeSource) codeSource.codeSource, loader)
            cu.addPhaseOperation(new AstNodeToScriptVisitor(writer, showScriptFreeForm, showScriptClass), compilePhase)
            cu.addSource(codeSource.getName(), script)
            try {
                cu.compile(compilePhase)
            } catch (CompilationFailedException cfe) {
                writer.println 'Unable to produce AST for this phase due to earlier compilation error:'
                cfe.message.eachLine {
                    writer.println it
                }
                writer.println 'Fix the above error(s) and then press Refresh'
            } catch (Throwable t) {
                writer.println 'Unable to produce AST for this phase due to an error:'
                writer.println t.getMessage()
                writer.println 'Fix the above error(s) and then press Refresh'
            }
        } finally {
            if (ownLoader) loader.close()
        }
        return writer.toString()
    }
}

/**
 * An adapter from ASTNode tree to source code.
 */
@AutoFinal @CompileStatic
class AstNodeToScriptVisitor implements CompilationUnit.IPrimaryClassNodeOperation, GroovyClassVisitor, GroovyCodeVisitor {

    private final Writer _out
    /** Stack of nested class names used while rendering constructors. */
    Stack<String> classNameStack = []
    /** Current indentation prefix. */
    String _indent = ''
    /** Indicates whether the next write should emit indentation first. */
    boolean readyToIndent = true
    /** Whether free-form script statements should be rendered. */
    boolean showScriptFreeForm
    /** Whether the generated script class should be rendered. */
    boolean showScriptClass
    /** Tracks whether the free-form script body has already been emitted. */
    boolean scriptHasBeenVisited

    /**
     * Creates a visitor that renders AST nodes as Groovy source.
     *
     * @param writer target writer
     * @param showScriptFreeForm whether free-form script statements should be rendered
     * @param showScriptClass whether the generated script class should be rendered
     */
    AstNodeToScriptVisitor(Writer writer, boolean showScriptFreeForm = true, boolean showScriptClass = true) {
        this._out = writer
        this.showScriptFreeForm = showScriptFreeForm
        this.showScriptClass = showScriptClass
        this.scriptHasBeenVisited = false
    }

    /**
     * Renders the supplied primary class node and optional script body.
     */
    @Override
    void call(SourceUnit source, GeneratorContext context, ClassNode classNode) {

        visitPackage(source?.getAST()?.getPackage())

        visitAllImports(source)

        if (showScriptFreeForm && !scriptHasBeenVisited) {
            scriptHasBeenVisited = true
            source?.getAST()?.getStatementBlock()?.visit(this)
        }
        if (showScriptClass || !classNode.isScript()) {
            visitClass classNode
        }
    }

    private void visitAllImports(SourceUnit source) {
        boolean staticImportsPresent = false
        boolean importsPresent = false

        source?.getAST()?.getStaticImports()?.values()?.each {
            visitImport(it)
            staticImportsPresent = true
        }
        source?.getAST()?.getStaticStarImports()?.values()?.each {
            visitImport(it)
            staticImportsPresent = true
        }

        if (staticImportsPresent) {
            printDoubleBreak()
        }

        source?.getAST()?.getImports()?.each {
            visitImport(it)
            importsPresent = true
        }
        source?.getAST()?.getStarImports()?.each {
            visitImport(it)
            importsPresent = true
        }
        if (importsPresent) {
            printDoubleBreak()
        }
    }

    /**
     * Writes text, applying the current indentation rules.
     *
     * @param parameter value to append to the output
     */
    void print(parameter) {
        def output = parameter.toString()

        if (readyToIndent) {
            _out.print _indent
            readyToIndent = false
            while (output.startsWith(' ')) {
                output = output[1..-1]  // trim left
            }
        }
        if (_out.toString().endsWith(' ')) {
            if (output.startsWith(' ')) {
                output = output[1..-1]
            }
        }
        _out.print output
    }

    /**
     * This visitor writes directly to its target and does not support println-style output.
     *
     * @param parameter ignored output parameter
     */
    def println(parameter) {
        throw new UnsupportedOperationException('Wrong API')
    }

    /**
     * Executes the supplied block one indentation level deeper.
     *
     * @param block block to execute while indented
     */
    def indented(Closure block) {
        String startingIndent = _indent
        _indent = _indent + '    '
        block()
        _indent = startingIndent
    }

    /**
     * Ensures the output ends with a single line break.
     */
    def printLineBreak() {
        if (!_out.toString().endsWith('\n')) {
            _out.print '\n'
        }
        readyToIndent = true
    }

    /**
     * Ensures the output ends with a blank line.
     */
    def printDoubleBreak() {
        if (_out.toString().endsWith('\n\n')) {
            // do nothing
        } else if (_out.toString().endsWith('\n')) {
            _out.print '\n'
        } else {
            _out.print '\n'
            _out.print '\n'
        }
        readyToIndent = true
    }

    /**
     * Writes the package declaration for the current source unit.
     *
     * @param packageNode package node to render
     */
    void visitPackage(PackageNode packageNode) {

        if (packageNode) {

            packageNode.annotations?.each {
                visitAnnotationNode(it)
                printLineBreak()
            }

            if (packageNode.text.endsWith('.')) {
                print packageNode.text[0..-2]
            } else {
                print packageNode.text
            }
            printDoubleBreak()
        }
    }

    /**
     * Writes an import declaration.
     *
     * @param node import node to render
     */
    void visitImport(ImportNode node) {
        if (node) {
            node.annotations?.each {
                visitAnnotationNode(it)
                printLineBreak()
            }
            print node.text
            printLineBreak()
        }
    }

    /** Renders a class or interface declaration. */
    @Override
    void visitClass(ClassNode node) {

        classNameStack.push(node.name)

        node?.annotations?.each {
            visitAnnotationNode(it)
            printLineBreak()
        }

        visitModifiers(node.modifiers)
        if (node.isInterface()) print node.name
        else print "class $node.name"
        visitGenerics node?.genericsTypes
        print ' extends '
        visitType node.unresolvedSuperClass
        boolean first = true
        node.unresolvedInterfaces?.each {
            if (!first) {
                print ', '
            } else {
                print ' implements '
            }
            first = false
            visitType it
        }
        print ' { '
        printDoubleBreak()

        indented {
            node?.properties?.each { visitProperty(it) }
            printLineBreak()
            node?.fields?.each { visitField(it) }
            printDoubleBreak()
            node?.declaredConstructors?.each { visitConstructor(it) }
            printLineBreak()
            visitObjectInitializerBlocks(node)
            printLineBreak()
            node?.methods?.each { visitMethod(it) }
        }
        print '}'
        printLineBreak()
        classNameStack.pop()
    }

    private void visitObjectInitializerBlocks(ClassNode node) {
        for (Statement stmt : node.getObjectInitializerStatements()) {
            print '{'
            printLineBreak()
            indented {
                stmt.visit(this)
            }
            printLineBreak()
            print '}'
            printDoubleBreak()
        }
    }

    private void visitGenerics(GenericsType[] generics) {

        if (generics) {
            print '<'
            boolean first = true
            generics.each { GenericsType it ->
                if (!first) {
                    print ', '
                }
                first = false
                print it.name
                if (!it.placeholder && !it.wildcard) {
                    visitGenerics it.type?.genericsTypes
                }
                if (it.upperBounds) {
                    print ' extends '
                    boolean innerFirst = true
                    it.upperBounds.each { ClassNode upperBound ->
                        if (!innerFirst) {
                            print ' & '
                        }
                        innerFirst = false
                        visitType upperBound
                    }
                }
                if (it.lowerBound) {
                    print ' super '
                    visitType it.lowerBound
                }
            }
            print '>'
        }
    }

    /** Renders a constructor declaration. */
    @Override
    void visitConstructor(ConstructorNode node) {
        visitMethod(node)
    }

    private void visitParameters(Parameter[] parameters) {
        int i = 0
        for (p in parameters) {
            if (i++ != 0) {
                print ', '
            }

            for (a in p.annotations) {
                visitAnnotationNode a
                print ' '
            }

            visitModifiers(p.modifiers)
            visitType p.type
            print ' ' + p.name
            if (p.initialExpression && p.initialExpression !instanceof EmptyExpression) {
                print ' = '
                p.initialExpression.visit this
            }
        }
    }

    /** Renders a method or initializer declaration. */
    @Override
    void visitMethod(MethodNode node) {
        node?.annotations?.each {
            visitAnnotationNode(it)
            printLineBreak()
        }

        visitModifiers(node.modifiers)
        if (node.name == '<init>') {
            print "${classNameStack.peek()}("
            visitParameters node.parameters
            print ') {'
            printLineBreak()
        } else if (node.name == '<clinit>') {
            print '{ ' // will already have 'static' from modifiers
            printLineBreak()
        } else {
            visitType node.returnType
            print " $node.name("
            visitParameters node.parameters
            print ')'
            if (node.exceptions) {
                boolean first = true
                print ' throws '
                node.exceptions.each {
                    if (!first) {
                        print ', '
                    }
                    first = false
                    visitType it
                }
            }
            print ' {'
            printLineBreak()
        }

        indented {
            node?.code?.visit(this)
        }
        printLineBreak()
        print '}'
        printDoubleBreak()
    }

    private void visitModifiers(int modifiers) {
        String mods = Modifier.toString(modifiers)
        mods = mods ? mods + ' ' : mods
        print mods
    }

    /** Renders a field declaration. */
    @Override
    void visitField(FieldNode node) {
        node?.annotations?.each {
            visitAnnotationNode(it)
            printLineBreak()
        }
        visitModifiers(node.modifiers)
        visitType node.type
        print " $node.name "
        // do not print initial expression, as this is executed as part of the constructor, unless on static constant
        Expression exp = node.initialValueExpression
        if (exp instanceof ConstantExpression) exp = Verifier.transformToPrimitiveConstantIfPossible(exp)
        ClassNode type = exp?.type
        if (Modifier.isStatic(node.modifiers) && Modifier.isFinal(node.getModifiers())
                && exp instanceof ConstantExpression
                && type == node.type
                && ClassHelper.isStaticConstantInitializerType(type)) {
            // GROOVY-5150: final constants may be initialized directly
            print ' = '
            if (ClassHelper.STRING_TYPE == type) {
                print "'" + node.initialValueExpression.text.replace("'", "\\'") + "'"
            } else if (ClassHelper.char_TYPE == type) {
                print "'${node.initialValueExpression.text}'"
            } else {
                print node.initialValueExpression.text
            }
        }
        printLineBreak()
    }

    /** Renders an annotation and its members. */
    void visitAnnotationNode(AnnotationNode node) {
        print '@' + node?.classNode?.name
        if (node?.members) {
            print '('
            boolean first = true
            node.members.each { String name, Expression value ->
                if (first) {
                    first = false
                } else {
                    print ', '
                }
                print name + ' = '
                value.visit(this)
            }
            print ')'
        }

    }

    /** Skips property rendering because fields already emit the corresponding output. */
    @Override
    void visitProperty(PropertyNode node) {
        // is a FieldNode, avoid double dispatch
    }

    /** Renders a block statement. */
    @Override
    void visitBlockStatement(BlockStatement block) {
        if (printStatementLabels(block)) {
            print '{'
            printLineBreak()
            indented {
                block?.statements?.each {
                    it.visit(this)
                    printLineBreak()
                }
            }
            print '}'
            printLineBreak()
        } else {
            block?.statements?.each {
                it.visit(this)
                printLineBreak()
            }
        }
        if (!_out.toString().endsWith('\n')) {
            printLineBreak()
        }
    }

    /** Renders a for-loop statement. */
    @Override
    void visitForLoop(ForStatement statement) {
        printStatementLabels(statement)
        print 'for ('
        if (statement.valueVariable) {
            if (statement.indexVariable) {
                visitParameters(statement.indexVariable)
                print ', '
            }
            visitParameters(statement.valueVariable)
            print ' : '
        }
        statement.collectionExpression.visit(this)
        print ') {'
        printLineBreak()
        indented {
            statement.loopBlock.visit(this)
        }
        print '}'
        printLineBreak()
    }

    /** Renders an if/else statement. */
    @Override
    void visitIfElse(IfStatement ifElse) {
        printStatementLabels(ifElse)
        print 'if ('
        ifElse?.booleanExpression?.visit this
        print ') {'
        printLineBreak()
        indented {
            ifElse?.ifBlock?.visit this
        }
        printLineBreak()
        if (ifElse?.elseBlock && !(ifElse.elseBlock instanceof EmptyStatement)) {
            print '} else {'
            printLineBreak()
            indented {
                ifElse?.elseBlock?.visit this
            }
            printLineBreak()
        }
        print '}'
        printLineBreak()
    }

    /** Renders a standalone expression statement. */
    @Override
    void visitExpressionStatement(ExpressionStatement statement) {
        statement.expression.visit this
    }

    /** Renders a return statement. */
    @Override
    void visitReturnStatement(ReturnStatement statement) {
        printLineBreak()
        print 'return '
        statement.getExpression().visit(this)
        printLineBreak()
    }

    /** Renders a first-class switch expression (GROOVY-12255). */
    @Override
    void visitSwitchExpression(SwitchExpression expression) {
        print 'switch ('
        expression?.expression?.visit this
        print ') {'
        printLineBreak()
        indented {
            expression?.caseStatements?.each {
                visitCaseStatement it
            }
            if (expression?.defaultStatement !instanceof EmptyStatement) {
                print 'default -> '
                printLineBreak()
                expression?.defaultStatement?.visit this
            }
        }
        print '}'
    }

    /** Renders a yield statement (GROOVY-12255). */
    @Override
    void visitYieldStatement(YieldStatement statement) {
        print 'yield '
        statement?.expression?.visit this
        printLineBreak()
    }

    /** Renders a switch statement. */
    @Override
    void visitSwitch(SwitchStatement statement) {
        printStatementLabels(statement)
        print 'switch ('
        statement?.expression?.visit this
        print ') {'
        printLineBreak()
        indented {
            statement?.caseStatements?.each {
                visitCaseStatement it
            }
            if (statement?.defaultStatement !instanceof EmptyStatement) {
                print 'default: '
                printLineBreak()
                statement?.defaultStatement?.visit this
            }
        }
        print '}'
        printLineBreak()
    }

    /** Renders a switch case. */
    @Override
    void visitCaseStatement(CaseStatement statement) {
        print 'case '
        statement?.expression?.visit this
        print ':'
        printLineBreak()
        indented {
            statement?.code?.visit this
        }
    }

    /** Renders a break statement. */
    @Override
    void visitBreakStatement(BreakStatement statement) {
        print 'break'
        if (statement?.label) {
            print ' ' + statement.label
        }
        printLineBreak()
    }

    /** Renders a continue statement. */
    @Override
    void visitContinueStatement(ContinueStatement statement) {
        print 'continue'
        if (statement?.label) {
            print ' ' + statement.label
        }
        printLineBreak()
    }

    /** Renders a method call expression. */
    @Override
    void visitMethodCallExpression(MethodCallExpression expression) {
        printExpression expression.objectExpression
        if (expression.spreadSafe) {
            print '*'
        }
        if (expression.safe) {
            print '?'
        }
        print '.'
        visitGenerics expression.genericsTypes
        Expression method = expression.method
        if (method instanceof ConstantExpression) {
            visitConstantExpression(method, true)
        } else {
            method.visit(this)
        }
        expression.arguments.visit(this)
    }

    /** Renders a static method call expression. */
    @Override
    void visitStaticMethodCallExpression(StaticMethodCallExpression expression) {
        boolean parens = expression?.arguments instanceof MethodCallExpression
                || expression?.arguments instanceof VariableExpression
        print expression?.ownerType?.name + '.' + expression?.method
        if (parens) print '('
        expression?.arguments?.visit this
        if (parens) print ')'
    }

    /** Renders a constructor invocation expression. */
    @Override
    void visitConstructorCallExpression(ConstructorCallExpression expression) {
        if (expression?.isSuperCall()) {
            print 'super'
        } else if (expression?.isThisCall()) {
            print 'this '
        } else {
            print 'new '
            visitType expression?.type
        }
        expression?.arguments?.visit this
    }

    /** Renders a binary or assignment expression. */
    @Override
    void visitBinaryExpression(BinaryExpression expression) {
        if (expression !instanceof DeclarationExpression && expression?.leftExpression instanceof VariableExpression) {
            visitVariableExpression((VariableExpression) expression.leftExpression, false)
        } else {
            expression?.leftExpression?.visit this
        }
        boolean isAssign = expression?.operation?.type == Types.ASSIGN
        boolean isAccess = expression?.operation?.type == Types.LEFT_SQUARE_BRACKET
        if (!isAssign || expression?.rightExpression !instanceof EmptyExpression) {
            print isAccess ? (expression.safe ? '?[' : '[') : " ${expression?.operation?.text} "
            expression?.rightExpression?.visit this
            if (isAccess) {
                print ']'
            }
        }
    }

    /** Renders a postfix expression. */
    @Override
    void visitPostfixExpression(PostfixExpression expression) {
        if (expression?.expression instanceof VariableExpression) {
            visitVariableExpression((VariableExpression) expression?.expression, false)
        } else {
            print '('
            expression?.expression?.visit this
            print ')'
        }
        print expression?.operation?.text
    }

    /** Renders a prefix expression. */
    @Override
    void visitPrefixExpression(PrefixExpression expression) {
        print expression?.operation?.text
        if (expression?.expression instanceof VariableExpression) {
            visitVariableExpression((VariableExpression) expression?.expression, false)
        } else {
            print '('
            expression?.expression?.visit this
            print ')'
        }
    }

    /** Renders a closure expression. */
    @Override
    void visitClosureExpression(ClosureExpression expression) {
        print '{ '
        if (expression?.parameters) {
            visitParameters expression?.parameters
            print ' ->'
        } else if (expression?.parameters == null) {
            print ' ->'
        }
        printLineBreak()
        indented {
            expression?.code?.visit this
        }
        print '}'
        printGeneratedClosureClass expression
    }

    /**
     * Appends, as a trailing block comment, the name of the synthetic class a closure (or lambda)
     * literal was compiled to. The mark is only present after the class-generation phase and only
     * for literals that became a generated closure class, so nothing is printed otherwise.
     */
    private void printGeneratedClosureClass(ClosureExpression expression) {
        def generatedClass = expression?.getNodeMetaData(ClosureWriter.GENERATED_CLOSURE_CLASS)
        if (generatedClass) {
            print " /* => ${generatedClass} */"
        }
    }

    /** Renders a lambda expression. */
    @Override
    void visitLambdaExpression(LambdaExpression expression) {
        print '('
        if (expression?.parameters) {
            visitParameters expression?.parameters
        }
        print ') -> {'
        printLineBreak()
        indented {
            expression?.code?.visit this
        }
        print '}'
        printGeneratedClosureClass expression
    }

    /** Renders a tuple expression. */
    @Override
    void visitTupleExpression(TupleExpression expression) {
        print '('
        printExpressions expression?.expressions
        print ')'
    }

    /** Renders a range expression. */
    @Override
    void visitRangeExpression(RangeExpression expression) {
        print '('
        expression?.from?.visit this
        if (expression.exclusiveLeft) print '<'
        print '..'
        if (expression.exclusiveRight) print '<'
        expression?.to?.visit this
        print ')'
    }

    /** Renders a property access expression. */
    @Override
    void visitPropertyExpression(PropertyExpression expression) {
        printExpression expression?.objectExpression
        if (expression?.spreadSafe) {
            print '*'
        } else if (expression?.isSafe()) {
            print '?'
        }
        print expression instanceof AttributeExpression ? '.@' : '.'
        if (expression?.property instanceof ConstantExpression) {
            visitConstantExpression((ConstantExpression) expression?.property, true)
        } else {
            expression?.property?.visit this
        }
    }

    /** Renders an attribute access expression. */
    @Override
    void visitAttributeExpression(AttributeExpression attributeExpression) {
        visitPropertyExpression attributeExpression
    }

    /** Renders a field reference expression. */
    @Override
    void visitFieldExpression(FieldExpression expression) {
        print expression?.field?.name
    }

    /** Renders a constant expression. */
    @Override
    void visitConstantExpression(ConstantExpression expression, boolean unwrapQuotes = false) {
        if (expression.value instanceof String && !unwrapQuotes) {
            print "'" + escapeSingleQuoted((String) expression.value) + "'"
        } else {
            print expression.value
            // re-append the literal's type suffix so re-parsing yields the same type
            // (Integer and BigDecimal are the literal defaults and need no suffix)
            def value = expression.value
            if (value instanceof Long) {
                print 'L'
            } else if (value instanceof Float) {
                print 'F'
            } else if (value instanceof Double) {
                print 'D'
            } else if (value instanceof BigInteger) {
                print 'G'
            }
        }
    }

    /**
     * Escapes a String so it re-parses as a single-line, single-quoted Groovy literal.
     * The former approach only handled newlines and the single quote, so a literal
     * backslash (or a tab, carriage-return, form-feed, etc.) was emitted verbatim,
     * either changing the string's value or producing a script that no longer compiles.
     * '$' is intentionally left alone: it is inert inside single quotes.
     */
    private static String escapeSingleQuoted(String value) {
        StringBuilder sb = new StringBuilder(value.length() + 8)
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i)
            switch (c) {
                case '\\' as char: sb.append('\\\\'); break   // must be first so we don't re-escape below
                case '\'' as char: sb.append('\\\''); break
                case '\n' as char: sb.append('\\n'); break
                case '\r' as char: sb.append('\\r'); break
                case '\t' as char: sb.append('\\t'); break
                case '\b' as char: sb.append('\\b'); break
                case '\f' as char: sb.append('\\f'); break
                default:
                    if ((int) c < 0x20) {
                        sb.append(String.format('\\u%04x', (int) c))
                    } else {
                        sb.append(c)
                    }
            }
        }
        return sb.toString()
    }

    /** Renders a class literal expression. */
    @Override
    void visitClassExpression(ClassExpression expression) {
        print expression.text
    }

    /** Renders a variable reference. */
    @Override
    void visitVariableExpression(VariableExpression expression, boolean spacePad = true) {
        if (spacePad) {
            print ' ' + expression.name + ' '
        } else {
            print expression.name
        }
    }

    /** Renders a declaration expression. */
    @Override
    void visitDeclarationExpression(DeclarationExpression expression) {
        if (!expression.isMultipleAssignmentDeclaration()) {
            visitType expression.leftExpression.type
            visitBinaryExpression expression
            return
        }

        print 'def ('
        int i = 0
        for (e in expression.tupleExpression) {
            if (i++ != 0) print ', '
            visitType e.type
            print ' '
            printExpression e
        }
        print ') = '
        expression.rightExpression.visit this
    }

    /** Renders a GString expression. */
    @Override
    void visitGStringExpression(GStringExpression expression) {
        // Rebuild from the node's parts rather than its verbatimText: verbatimText holds the
        // *decoded* text (so a literal '"', '\\' or '$' would break re-parsing) and renders each
        // embedded value via getText() (which is lossy, e.g. dropping string-literal quotes).
        // Escaping the text segments and re-visiting the values keeps the emitted GString faithful.
        List<ConstantExpression> strings = expression.strings
        List<Expression> values = expression.values
        print '"'
        for (int i = 0; i < strings.size(); i++) {
            String following = (String) strings[i].value
            print escapeDoubleQuotedText(following)
            if (i < values.size() && values[i] != null) {
                Expression value = values[i]
                String nextText = (i + 1 < strings.size()) ? (String) strings[i + 1].value : ''
                // '$name' short form is only safe when the following text can't extend it into a
                // different variable ('$nameX') or property ('$name.p'); otherwise use '${name}'.
                if (value instanceof VariableExpression && !((VariableExpression) value).isThisExpression()
                        && (nextText.isEmpty() || !(Character.isJavaIdentifierPart(nextText.charAt(0)) || nextText.charAt(0) == '.' as char))) {
                    print '$' + ((VariableExpression) value).name
                } else {
                    print '${'
                    printExpression value
                    print '}'
                }
            }
        }
        print '"'
    }

    /**
     * Escapes a String so it survives re-parsing inside a double-quoted GString's text segment.
     * Beyond the usual control characters, '"' (delimiter), '\\' (escape) and '$' (interpolation
     * trigger) must all be escaped so literal text is never mistaken for structure.
     */
    private static String escapeDoubleQuotedText(String value) {
        StringBuilder sb = new StringBuilder(value.length() + 8)
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i)
            switch (c) {
                case '\\' as char: sb.append('\\\\'); break   // must be first
                case '"' as char:  sb.append('\\"'); break
                case '$' as char:  sb.append('\\$'); break
                case '\n' as char: sb.append('\\n'); break
                case '\r' as char: sb.append('\\r'); break
                case '\t' as char: sb.append('\\t'); break
                case '\b' as char: sb.append('\\b'); break
                case '\f' as char: sb.append('\\f'); break
                default:
                    if ((int) c < 0x20) {
                        sb.append(String.format('\\u%04x', (int) c))
                    } else {
                        sb.append(c)
                    }
            }
        }
        return sb.toString()
    }

    /** Renders a spread expression. */
    @Override
    void visitSpreadExpression(SpreadExpression expression) {
        print '*'
        expression?.expression?.visit this
    }

    @CompileDynamic
    private void printUnaryExpression(String opText, Expression expression) {
        print opText
        if (expression?.expression instanceof VariableExpression) {
            visitVariableExpression((VariableExpression) expression?.expression, false)
        } else if (expression?.expression instanceof PropertyExpression)  {
            expression?.expression?.visit this
        } else {
            print '('
            expression?.expression?.visit this
            print ')'
        }
    }

    /** Renders a logical negation expression. */
    @Override
    void visitNotExpression(NotExpression expression) {
        printUnaryExpression('!', expression)
    }

    /** Renders a unary minus expression. */
    @Override
    void visitUnaryMinusExpression(UnaryMinusExpression expression) {
        printUnaryExpression('-', expression)
    }

    /** Renders a unary plus expression. */
    @Override
    void visitUnaryPlusExpression(UnaryPlusExpression expression) {
        printUnaryExpression('+', expression)
    }

    /** Renders a cast or coercion expression. */
    @Override
    void visitCastExpression(CastExpression expression) {
        print '('
        if (expression?.coerce) {
            boolean isVariableExpressionOrPropertyExpression =
                    expression?.expression instanceof VariableExpression || expression?.expression instanceof PropertyExpression
            if (!isVariableExpressionOrPropertyExpression) {
                print '('
            }
            printExpression(expression?.expression)
            if (!isVariableExpressionOrPropertyExpression) {
                print ')'
            }
            print ' as '
            visitType(expression?.type)
        } else {
            print '('
            visitType(expression?.type)
            print ') '
            printExpression(expression?.expression)
        }
        print ')'
    }

    /**
     * Prints out the type, safely handling arrays.
     *
     * @param classNode type to render
     */
    void visitType(ClassNode classNode) {
        def name = classNode.name
        if (name =~ /^\[+L/ && name.endsWith(';')) {
            int numDimensions = name.indexOf('L')
            print "${classNode.name[(numDimensions + 1)..-2]}" + ('[]' * numDimensions)
        } else {
            print name
        }
        visitGenerics classNode?.genericsTypes
    }

    /** Emits a placeholder for a bytecode expression. */
    @Override
    void visitBytecodeExpression(BytecodeExpression expression) {
        print '/*BytecodeExpression*/'
        printLineBreak()
    }

    /** Renders a map expression. */
    @Override
    void visitMapExpression(MapExpression expression) {
        print '['
        if (!expression?.mapEntryExpressions) {
            print ':'
        } else {
            printExpressions expression.mapEntryExpressions
        }
        print ']'
    }

    /** Renders a map entry expression. */
    @Override
    void visitMapEntryExpression(MapEntryExpression expression) {
        if (expression?.keyExpression instanceof SpreadMapExpression) {
            print '*'            // is this correct?
        } else {
            expression?.keyExpression?.visit this
        }
        print ': '
        expression?.valueExpression?.visit this
    }

    /** Renders a list expression. */
    @Override
    void visitListExpression(ListExpression expression) {
        print '['
        printExpressions expression?.expressions
        print ']'
    }

    /** Renders a try/catch/finally statement. */
    @Override
    void visitTryCatchFinally(TryCatchStatement statement) {
        printStatementLabels(statement)
        print 'try {'
        printLineBreak()
        indented {
            statement?.tryStatement?.visit this
        }
        printLineBreak()
        print '} '
        printLineBreak()
        statement?.catchStatements?.each { CatchStatement catchStatement ->
            visitCatchStatement(catchStatement)
        }
        if (statement?.finallyStatement !instanceof EmptyStatement) {
            print 'finally { '
            printLineBreak()
            indented {
                statement?.finallyStatement?.visit this
            }
            print '} '
            printLineBreak()
        }
    }

    /** Renders a throw statement. */
    @Override
    void visitThrowStatement(ThrowStatement statement) {
        print 'throw '
        statement?.expression?.visit this
        printLineBreak()
    }

    /** Renders a synchronized statement. */
    @Override
    void visitSynchronizedStatement(SynchronizedStatement statement) {
        printStatementLabels(statement)
        print 'synchronized ('
        statement?.expression?.visit this
        print ') {'
        printLineBreak()
        indented {
            statement?.code?.visit this
        }
        print '}'
    }

    /** Renders a ternary expression. */
    @Override
    void visitTernaryExpression(TernaryExpression expression) {
        expression?.booleanExpression?.visit this
        print ' ? '
        expression?.trueExpression?.visit this
        print ' : '
        expression?.falseExpression?.visit this
    }

    /** Renders an Elvis expression. */
    @Override
    void visitShortTernaryExpression(ElvisOperatorExpression expression) {
        expression?.booleanExpression?.visit this
        print ' ?: '
        expression?.falseExpression?.visit this
    }

    /** Renders a boolean expression. */
    @Override
    void visitBooleanExpression(BooleanExpression expression) {
        printExpression expression?.expression
    }

    /** Renders a while-loop statement. */
    @Override
    void visitWhileLoop(WhileStatement statement) {
        printStatementLabels(statement)
        print 'while ('
        statement?.booleanExpression?.visit this
        print ') {'
        printLineBreak()
        indented {
            statement?.loopBlock?.visit this
        }
        printLineBreak()
        print '}'
        printLineBreak()
    }

    /** Renders a do/while-loop statement. */
    @Override
    void visitDoWhileLoop(DoWhileStatement statement) {
        printStatementLabels(statement)
        print 'do {'
        printLineBreak()
        indented {
            statement?.loopBlock?.visit this
        }
        print '} while ('
        statement?.booleanExpression?.visit this
        print ')'
        printLineBreak()
    }

    /** Renders a catch clause. */
    @Override
    void visitCatchStatement(CatchStatement statement) {
        print 'catch ('
        visitParameters statement.variable
        print ') {'
        printLineBreak()
        indented {
            statement.code?.visit this
        }
        print '} '
        printLineBreak()
    }

    /** Renders a bitwise negation expression. */
    @Override
    void visitBitwiseNegationExpression(BitwiseNegationExpression expression) {
        print '~('
        expression?.expression?.visit this
        print ') '
    }

    /** Renders an assert statement. */
    @Override
    void visitAssertStatement(AssertStatement statement) {
        print 'assert '
        statement?.booleanExpression?.visit this
        print ' : '
        statement?.messageExpression?.visit this
    }

    /**
     * Retained for compatibility; delegates to {@link #visitArgumentlistExpression(ArgumentListExpression)}.
     */
    @Deprecated
    void visitArgumentlistExpression(ArgumentListExpression expression, boolean showTypesIgnored) {
        visitArgumentlistExpression(expression)
    }

    /** Renders an argument list expression. */
    @Override
    void visitArgumentlistExpression(ArgumentListExpression expression) {
        visitTupleExpression expression
    }

    /** Renders a closure list expression. */
    @Override
    void visitClosureListExpression(ClosureListExpression expression) {
        int i = 0
        for (e in expression?.expressions) {
            if (i++ != 0) {
                print ';'
                if (e !instanceof EmptyExpression) {
                    print ' '
                }
            }
            e.visit this
        }
    }

    /** Renders a method pointer expression. */
    @Override
    void visitMethodPointerExpression(MethodPointerExpression expression) {
        expression?.expression?.visit this
        print '.&'
        expression?.methodName?.visit this
    }

    /** Renders a method reference expression. */
    @Override
    void visitMethodReferenceExpression(MethodReferenceExpression expression) {
        expression?.expression?.visit this
        print '::'
        expression?.methodName?.visit this
    }

    /** Renders an array expression. */
    @Override
    void visitArrayExpression(ArrayExpression expression) {
        print 'new '
        visitType expression?.elementType
        print '['
        printExpressions expression?.sizeExpression
        print ']'
    }

    /** Renders a spread-map expression. */
    @Override
    void visitSpreadMapExpression(SpreadMapExpression expression) {
        print '*:'
        expression?.expression?.visit this
    }

    //--------------------------------------------------------------------------

    private void printExpression(Expression expression) {
        if (expression instanceof VariableExpression) {
            visitVariableExpression expression, false
        } else {
            expression?.visit this
        }
    }

    private void printExpressions(List<? extends Expression> expressions) {
        int i = 0
        for (e in expressions) {
            if (i++ != 0) {
                print ', '
            }
            printExpression e
        }
    }

    /**
     * Prints all labels for the given statement.  The labels will be printed on a single
     * line and line break will be added.
     *
     * @param statement for which to print labels
     * @return {@code true} if the statement had labels to print, else {@code false}
     */
    private boolean printStatementLabels(Statement statement) {
        List<String> labels = statement?.statementLabels
        if (labels == null || labels.isEmpty()) {
            return false
        }
        for (String label : labels) {
            print label + ':'
            printLineBreak()
        }
        return true
    }
}
