
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2019 Arno Unkrig. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.codehaus.janino.tests;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.Location;
import org.codehaus.commons.nullanalysis.Nullable;
import org.codehaus.janino.Java;
import org.codehaus.janino.Java.AbstractClassDeclaration;
import org.codehaus.janino.Java.AbstractCompilationUnit;
import org.codehaus.janino.Java.AmbiguousName;
import org.codehaus.janino.Java.Annotation;
import org.codehaus.janino.Java.ArrayType;
import org.codehaus.janino.Java.Assignment;
import org.codehaus.janino.Java.Block;
import org.codehaus.janino.Java.BlockStatement;
import org.codehaus.janino.Java.CompilationUnit;
import org.codehaus.janino.Java.EmptyStatement;
import org.codehaus.janino.Java.ExpressionStatement;
import org.codehaus.janino.Java.FieldAccessExpression;
import org.codehaus.janino.Java.FieldDeclaration;
import org.codehaus.janino.Java.FloatingPointLiteral;
import org.codehaus.janino.Java.FunctionDeclarator.FormalParameters;
import org.codehaus.janino.Java.IntegerLiteral;
import org.codehaus.janino.Java.LabeledStatement;
import org.codehaus.janino.Java.LocalVariableDeclarationStatement;
import org.codehaus.janino.Java.MethodDeclarator;
import org.codehaus.janino.Java.PackageMemberClassDeclaration;
import org.codehaus.janino.Java.Primitive;
import org.codehaus.janino.Java.PrimitiveType;
import org.codehaus.janino.Java.ReturnStatement;
import org.codehaus.janino.Java.Rvalue;
import org.codehaus.janino.Java.ThisReference;
import org.codehaus.janino.Java.Type;
import org.codehaus.janino.Java.TypeDeclaration;
import org.codehaus.janino.Java.VariableDeclarator;
import org.codehaus.janino.Parser;
import org.codehaus.janino.Scanner;
import org.codehaus.janino.SimpleCompiler;
import org.codehaus.janino.Unparser;
import org.codehaus.janino.util.DeepCopier;
import org.junit.Assert;
import org.junit.Test;

// SUPPRESS CHECKSTYLE JavadocMethod:9999

/**
 * Tests for 'programmatically created' ASTs.
 */
public
class AstTest {

    private static Object
    compileAndEval(AbstractCompilationUnit acu) throws Exception {
        SimpleCompiler compiler = new SimpleCompiler();
        compiler.cook(acu);

        ClassLoader loader = compiler.getClassLoader();

        Class<?> handMadeClass = loader.loadClass("HandMade");

        Object handMade = handMadeClass.newInstance();
        Method calc     = handMadeClass.getMethod("calculate", new Class[0]);
        Object res      = calc.invoke(handMade, new Object[0]);
        return res;
    }

    private static ArrayType
    createByteArrayType() {
        return new Java.ArrayType(new Java.PrimitiveType(AstTest.getLocation(), Java.Primitive.BYTE));
    }

    private static PackageMemberClassDeclaration
    createClass(CompilationUnit cu) {
        PackageMemberClassDeclaration clazz = new PackageMemberClassDeclaration(
            AstTest.getLocation(),
            null,
            new Java.Modifier[] { new Java.AccessModifier("public", AstTest.getLocation()) },
            "HandMade",
            null,         // typeParameters
            null,         // extendedType
            new Type[0]   // implementedTypes
        );
        cu.addPackageMemberTypeDeclaration(clazz);
        return clazz;
    }

    private static Type
    createDoubleType() { return new PrimitiveType(AstTest.getLocation(), Primitive.DOUBLE); }

    private static Java.BinaryOperation
    createOp(Rvalue l1, String op, Rvalue l2) { return new Java.BinaryOperation(AstTest.getLocation(), l1, op, l2); }

    private static IntegerLiteral
    createIntegerLiteral(String value) { return new IntegerLiteral(AstTest.getLocation(), value); }

    private static FloatingPointLiteral
    createFloatingPointLiteral(String value) { return new FloatingPointLiteral(AstTest.getLocation(), value); }

    private static void
    createMethod(PackageMemberClassDeclaration clazz, List<? extends Java.BlockStatement> statements, Type returnType) {
        MethodDeclarator method = new MethodDeclarator(
            AstTest.getLocation(),                                                            // location
            null,                                                                             // docComment
            new Java.Modifier[] { new Java.AccessModifier("public", AstTest.getLocation()) }, // modifiers
            null,                                                                             // typeParameters
            returnType,                                                                       // type
            "calculate",                                                                      // name
            new FormalParameters(AstTest.getLocation()),                                      // parameters
            new Type[0],                                                                      // thrownExceptions
            null,                                                                             // defaultValue
            statements                                                                        // statements
        );
        clazz.addDeclaredMethod(method);
    }

    private static LocalVariableDeclarationStatement
    createVarDecl(String name, String fPValue) {
        return new Java.LocalVariableDeclarationStatement(
            AstTest.getLocation(),
            new Java.Modifier[0],
            AstTest.createDoubleType(),
            new Java.VariableDeclarator[] {
                new Java.VariableDeclarator(
                    AstTest.getLocation(),
                    name,
                    0,
                    AstTest.createFloatingPointLiteral(fPValue)
                )
            }
        );
    }

    private static AmbiguousName
    createVariableRef(String name) {
        return new Java.AmbiguousName(AstTest.getLocation(), new String[] { name });
    }

    /**
     * A "Clever" method to get a location from a stack trace.
     */
    private static Location
    getLocation() {
        Exception         e   = new Exception();
        StackTraceElement ste = e.getStackTrace()[1]; //we only care about our caller
        return new Location(
            ste.getFileName(),
            ste.getLineNumber(),
            0
        );
    }

    @Test public void
    testBlock() throws Exception {
        CompilationUnit cu = new CompilationUnit("AstTests.java");

        final PackageMemberClassDeclaration clazz = AstTest.createClass(cu);

        List<Java.Statement> body = new ArrayList<>();

        Block sub = new Block(AstTest.getLocation());
        sub.addStatement(AstTest.createVarDecl("x", "2.0"));

        body.add(sub);
        body.add(
            new ReturnStatement(
                AstTest.getLocation(),
                new Java.BinaryOperation(
                    AstTest.getLocation(),
                    AstTest.createVariableRef("x"),
                    "*",
                    AstTest.createIntegerLiteral("3")
                )
            )
        );

        AstTest.createMethod(clazz, body, AstTest.createDoubleType());

        try {
            AstTest.compileAndEval(cu);
            Assert.fail("Block must limit the scope of variables in it");
        } catch (CompileException ex) {
            Assert.assertTrue(ex.getMessage().endsWith("Expression \"x\" is not an rvalue"));
        }
    }

    @Test public void
    testByteArrayLiteral() throws Exception {
        CompilationUnit cu = new CompilationUnit("AstTests.java");

        PackageMemberClassDeclaration clazz = AstTest.createClass(cu);

        final Byte           exp  = Byte.valueOf((byte) 1);
        List<Java.Statement> body = new ArrayList<>();
        body.add(
            new ReturnStatement(
                AstTest.getLocation(),
                new Java.NewInitializedArray(
                    AstTest.getLocation(),
                    AstTest.createByteArrayType(),
                    new Java.ArrayInitializer(
                        AstTest.getLocation(),
                        new Java.Rvalue[] { AstTest.createIntegerLiteral("1") }
                    )
                )
            )
        );

        AstTest.createMethod(clazz, body, AstTest.createByteArrayType());

        Object res = AstTest.compileAndEval(cu);
        Assert.assertEquals(exp.byteValue(), ((byte[]) res)[0]);
    }

    @Test public void
    testLocalVariable() throws Exception {
        CompilationUnit cu = new CompilationUnit("AstTests.java");

        final PackageMemberClassDeclaration clazz = AstTest.createClass(cu);

        List<Java.Statement> body = new ArrayList<>();
        body.add(AstTest.createVarDecl("x", "2.0"));
        body.add(
            new ReturnStatement(
                AstTest.getLocation(),
                new Java.BinaryOperation(
                    AstTest.getLocation(),
                    AstTest.createVariableRef("x"),
                    "*",
                    AstTest.createIntegerLiteral("3")
                )
            )
        );

        AstTest.createMethod(clazz, body, AstTest.createDoubleType());

        Object res = AstTest.compileAndEval(cu);
        Assert.assertTrue(res instanceof Double);
        Assert.assertEquals(Double.valueOf(6.0), res);
    }

    @Test public void
    testSimpleAst() throws Exception {
        CompilationUnit cu = new CompilationUnit("AstTests.java");

        PackageMemberClassDeclaration clazz = AstTest.createClass(cu);

        List<Java.Statement> body = new ArrayList<>();
        body.add(
            new ReturnStatement(
                AstTest.getLocation(),
                AstTest.createFloatingPointLiteral("3.0")
            )
        );

        AstTest.createMethod(clazz, body, AstTest.createDoubleType());

        Object res = AstTest.compileAndEval(cu);
        Assert.assertEquals(Double.valueOf(3.0), res);
    }

    @Test public void
    testClassRef() throws Exception {
        CompilationUnit cu = new CompilationUnit("AstTests.java");

        PackageMemberClassDeclaration clazz = AstTest.createClass(cu);

        List<Java.Statement> body = new ArrayList<>();

        body.add(
            new ReturnStatement(
                AstTest.getLocation(),
                new Java.ClassLiteral(
                    AstTest.getLocation(),
                    new Java.ReferenceType(
                        AstTest.getLocation(),       // location
                        new Annotation[0],           // annotations
                        new String[] { "HandMade" }, // identifiers
                        null                         // typeArguments
                    )
                )
            )
        );

        AstTest.createMethod(clazz, body, new Java.ReferenceType(
            AstTest.getLocation(),                    // location
            new Annotation[0],                        // annotations,
            new String[] { "java", "lang", "Class" }, // identifiers
            null                                      // typeArguments
        ));

        SimpleCompiler compiler = new SimpleCompiler();
        compiler.cook(cu);

        ClassLoader loader        = compiler.getClassLoader();
        Class<?>    handMadeClass = loader.loadClass("HandMade");
        Method      calc          = handMadeClass.getMethod("calculate", new Class[0]);

        Object handMade = handMadeClass.newInstance();
        Object res      = calc.invoke(handMade, new Object[0]);
        Assert.assertEquals(handMadeClass, res);
    }

    @Test public void
    testPrecedence() throws Exception {
        ExpressionStatement es = new Java.ExpressionStatement(
            new Java.Assignment(
                AstTest.getLocation(),
                new Java.AmbiguousName(AstTest.getLocation(), new String[] { "x" }),
                "=",
                AstTest.createOp(
                    AstTest.createIntegerLiteral("1"),
                    "*",
                    AstTest.createOp(AstTest.createIntegerLiteral("2"), "+", AstTest.createIntegerLiteral("3"))
                )
            )
        );

        StringWriter sw = new StringWriter();
        Unparser     u  = new Unparser(sw);
        u.unparseBlockStatement(es);
        u.close();
        Assert.assertEquals("x = 1 * ((( 2 + 3 )));", sw.toString());
    }

    /**
     * See <a href="https://github.com/janino-compiler/janino/issues/71">issue #71</a>: How can I move all local
     * variable declarations to field declarations using janino?
     */
    @Test public void
    testMoveLocalVariablesToFields() throws Exception {

        // Parse the input CU.
        AbstractCompilationUnit cu = new Parser(new Scanner(null, new StringReader(
            ""
            + " class A {"
            + " "
            + "     void test() {"
            + "         int b = 0;"
            + "         int c = 1;"
            + "         b += c;"
            + "     }"
            + " "
            + "     int a;"
            + " }"
        ))).parseAbstractCompilationUnit();

        // Now copy the input CU and modify it on-the-fly.
        cu = new DeepCopier() {

            private final List<FieldDeclaration> moreFieldDeclarations = new ArrayList<>();

            @Override public BlockStatement
            copyLocalVariableDeclarationStatement(LocalVariableDeclarationStatement lvds) throws CompileException {

                /**
                 * Generate synthetic fields for each local variable.
                 */
                List<VariableDeclarator> fieldVariableDeclarators = new ArrayList<>();
                for (VariableDeclarator vd : lvds.variableDeclarators) {
                    fieldVariableDeclarators.add(new VariableDeclarator(
                        vd.getLocation(),
                        vd.name,
                        vd.brackets,
                        null // initializer <= Do NOT copy the initializer!
                    ));
                }
                this.moreFieldDeclarations.add(new FieldDeclaration(
                    Location.NOWHERE,                 // location
                    null,                             // docComment
                    lvds.modifiers,                   // modifiers
                    this.copyType(lvds.type),         // type
                    fieldVariableDeclarators.toArray( // variableDeclarators
                        new VariableDeclarator[fieldVariableDeclarators.size()]
                    )
                ));

                /**
                 * Replace each local variable declaration with an assignment expression statement.
                 */
                List<BlockStatement> assignments = new ArrayList<>();
                for (VariableDeclarator vd : lvds.variableDeclarators) {

                    Rvalue initializer = (Rvalue) vd.initializer;
                    if (initializer == null) continue;

                    assignments.add(new ExpressionStatement(new Assignment(
                        Location.NOWHERE,            // location
                        new FieldAccessExpression(   // lhs
                            Location.NOWHERE,                    // location
                            new ThisReference(Location.NOWHERE), // lhs
                            vd.name                              // field
                        ),
                        "=",                         // operator
                        this.copyRvalue(initializer) // rhs
                    )));
                }

                if (assignments.isEmpty()) return new EmptyStatement(Location.NOWHERE);

                if (assignments.size() == 1) return assignments.get(0);

                Block result = new Block(Location.NOWHERE);
                result.addStatements(assignments);
                return result;
            }

            /**
             * Add the synthetic field declarations to the class.
             */
            @Override public TypeDeclaration
            copyPackageMemberClassDeclaration(PackageMemberClassDeclaration pmcd) throws CompileException {

                assert this.moreFieldDeclarations.isEmpty();
                try {
                    AbstractClassDeclaration
                    result = (AbstractClassDeclaration) super.copyPackageMemberClassDeclaration(pmcd);

                    for (FieldDeclaration fd : this.moreFieldDeclarations) {
                        result.addFieldDeclaration(fd); // TODO: Check for name clashes
                    }
                    return result;
                } finally {
                    this.moreFieldDeclarations.clear();
                }
            }

        }.copyAbstractCompilationUnit(cu);

        // Verify the transformation result.
        AstTest.assertUnparsesTo((
            ""
            + " class A {"
            + "     void test() {"
            + "         this.b = 0;" // <= Local variable initialize replaced with field assignment
            + "         this.c = 1;" // <=
            + "         b += c;"
            + "     }"
            + "     int a;"
            + "     int b;"          // <= Synthetic field that replaces the original local variable
            + "     int c;"          // <=
            + " }"
        ), cu);
    }

    @Test public void
    testFullyQualifiedFieldRef() throws Exception {
        CompilationUnit cu = new CompilationUnit("AstTests.java");

        PackageMemberClassDeclaration clazz = AstTest.createClass(cu);

        List<Java.Statement> body = new ArrayList<>();
        body.add(new Java.ReturnStatement(
            AstTest.getLocation(),
            new Java.FieldAccessExpression(
                AstTest.getLocation(),
                new Java.AmbiguousName(
                    AstTest.getLocation(),
                    new String[] { "other_package2", "ScopingRules" }
                ),
                "publicStaticDouble"
            )
        ));

        AstTest.createMethod(clazz, body, AstTest.createDoubleType());

        Object res = AstTest.compileAndEval(cu);
        Assert.assertEquals(other_package2.ScopingRules.publicStaticDouble, res);
    }

    @Test public void
    testDeepCopier() throws Exception {

        for (File f : UnparserTest.findJaninoJavaFiles()) {

            // Parse the compilation unit.
            AbstractCompilationUnit acu1 = AstTest.parseAbstractCompilationUnit(f);

            // Use the "DeepCopier" to copy it.
            AbstractCompilationUnit acu2 = new DeepCopier().copyAbstractCompilationUnit(acu1);

            // Assert that the copy is identical with the original.
            Assert.assertEquals(f.getPath(), AstTest.unparse(acu1), AstTest.unparse(acu2));
        }
    }

    private static AbstractCompilationUnit
    parseAbstractCompilationUnit(File f) throws CompileException, IOException {

        Reader r = new FileReader(f);
        try {
            return AstTest.parseAbstractCompilationUnit(f.getPath(), r);
        } finally {
            try { r.close(); } catch (Exception e) {}
        }
    }

    private static AbstractCompilationUnit
    parseAbstractCompilationUnit(@Nullable String fileName, Reader in) throws CompileException, IOException {
        return new Parser(new Scanner(fileName, in)).parseAbstractCompilationUnit();
    }

    /**
     * Parses a method declaration and transforms it into a labeled statement.
     *
     * @see <a href="https://github.com/janino-compiler/janino/issues/61">Issue #61</a>
     */
    @Test public void
    testMethodToLabeledStatement() throws Exception {
        String text1 = (
            ""
            + "public void eval() {\n"
            + "    if (in.isSet == 0 || in.end == in.start) {\n"
            + "        out.isSet = 0;\n"
            + "        return;\n"
            + "    }\n"
            + "    out.isSet = 1;\n"
            + "    out.start = 0;\n"
            + "    out.scale = scale.value;\n"
            + "    out.precision = precision.value;\n"
            + "\n"
            + "    byte[] buf = new byte[in.end - in.start];\n"
            + "\n"
            + "    in.buffer.getBytes(in.start, buf, 0, in.end - in.start);\n"
            + "\n"
            + "    String s = new String(buf, com.google.common.base.Charsets.UTF_8);\n"
            + "    java.math.BigDecimal bd = new java.math.BigDecimal(s);\n"
            + "\n"
            + "    org.apache.drill.exec.util.DecimalUtility.checkValueOverflow(bd, precision.value, scale.value);\n"
            + "    bd = bd.setScale(scale.value, java.math.RoundingMode.HALF_UP);\n"
            + "\n"
            + "    byte[] bytes = bd.unscaledValue().toByteArray();\n"
            + "    int len = bytes.length;\n"
            + "\n"
            + "    out.buffer = buffer.reallocIfNeeded(len);\n"
            + "    out.buffer.setBytes(out.start, bytes);\n"
            + "    out.end = out.start + len;\n"
            + "}"
        );

        final String text2 = (
            ""
            + "CastEmptyStringNullableVarCharToNullableVarDecimal_eval: {\n"
            + "    if (in.isSet == 0 || in.end == in.start) {\n"
            + "        out.isSet = 0;\n"
            + "        break CastEmptyStringNullableVarCharToNullableVarDecimal_eval;\n"
            + "    }\n"
            + "    out.isSet = 1;\n"
            + "    out.start = 0;\n"
            + "    out.scale = scale.value;\n"
            + "    out.precision = precision.value;\n"
            + "\n"
            + "    byte[] buf = new byte[in.end - in.start];\n"
            + "\n"
            + "    in.buffer.getBytes(in.start, buf, 0, in.end - in.start);\n"
            + "\n"
            + "    String s = new String(buf, com.google.common.base.Charsets.UTF_8);\n"
            + "    java.math.BigDecimal bd = new java.math.BigDecimal(s);\n"
            + "\n"
            + "    org.apache.drill.exec.util.DecimalUtility.checkValueOverflow(bd, precision.value, scale.value);\n"
            + "    bd = bd.setScale(scale.value, java.math.RoundingMode.HALF_UP);\n"
            + "\n"
            + "    byte[] bytes = bd.unscaledValue().toByteArray();\n"
            + "    int len = bytes.length;\n"
            + "\n"
            + "    out.buffer = buffer.reallocIfNeeded(len);\n"
            + "    out.buffer.setBytes(out.start, bytes);\n"
            + "    out.end = out.start + len;\n"
            + "}"
        );

        // Parse the method and get its body.
        MethodDeclarator md1 = new Parser(new Scanner(null, new StringReader(text1))).parseMethodDeclaration();

        List<? extends BlockStatement> ss = md1.statements;

        // Now generate a "labeled statement".
        LabeledStatement ls2;
        {
            final String label = "CastEmptyStringNullableVarCharToNullableVarDecimal_" + md1.name;
            Block        b     = new Block(md1.getLocation());

            b.addStatements(new DeepCopier() {

                @Override public BlockStatement
                copyReturnStatement(ReturnStatement subject) {
                    return new Java.BreakStatement(subject.getLocation(), label);
                }

            }.copyBlockStatements(ss));
            ls2 = new LabeledStatement(md1.getLocation(), label, b);
        }

        // Unparse the labeled statement.
        String actual;
        {
            StringWriter sw       = new StringWriter();
            Unparser     unparser = new Unparser(sw);

            unparser.unparseBlockStatement(ls2);
            unparser.close();
            actual = sw.toString();
        }

        Assert.assertEquals(
            UnparserTest.normalizeWhitespace(text2),
            UnparserTest.normalizeWhitespace(actual)
        );
    }

    public static String
    unparse(AbstractCompilationUnit acu) {
        StringWriter sw = new StringWriter();
        Unparser.unparse(acu, sw);
        return sw.toString();
    }

    public static void
    assertUnparsesTo(String expected, AbstractCompilationUnit acu) {
        Assert.assertEquals(
            UnparserTest.normalizeWhitespace(expected),
            UnparserTest.normalizeWhitespace(AstTest.unparse(acu))
        );
    }
}
