/*
 * BytecodeAstLanguage.java
 *
 * Copyright (c) 2013 Mike Strobel
 *
 * This source code is based on Mono.Cecil from Jb Evain, Copyright (c) Jb Evain;
 * and ILSpy/ICSharpCode from SharpDevelop, Copyright (c) AlphaSierraPapa.
 *
 * This source code is subject to terms and conditions of the Apache License, Version 2.0.
 * A copy of the license can be found in the License.html file at the root of this distribution.
 * By using this source code in any fashion, you are agreeing to be bound by the terms of the
 * Apache License, Version 2.0.
 *
 * You must not remove this notice, or any other, from this software.
 */

package com.strobel.decompiler.languages;

import com.strobel.assembler.metadata.Flags;
import com.strobel.assembler.metadata.MethodBody;
import com.strobel.assembler.metadata.MethodDefinition;
import com.strobel.assembler.metadata.ParameterDefinition;
import com.strobel.assembler.metadata.TypeDefinition;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.core.ArrayUtilities;
import com.strobel.core.ExceptionUtilities;
import com.strobel.core.StringUtilities;
import com.strobel.core.VerifyArgument;
import com.strobel.decompiler.DecompilationOptions;
import com.strobel.decompiler.DecompilerContext;
import com.strobel.decompiler.DecompilerHelpers;
import com.strobel.decompiler.ITextOutput;
import com.strobel.decompiler.NameSyntax;
import com.strobel.decompiler.PlainTextOutput;
import com.strobel.decompiler.ast.AstBuilder;
import com.strobel.decompiler.ast.AstOptimizationStep;
import com.strobel.decompiler.ast.AstOptimizer;
import com.strobel.decompiler.ast.Block;
import com.strobel.decompiler.ast.Expression;
import com.strobel.decompiler.ast.Variable;

import javax.lang.model.element.Modifier;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public class BytecodeAstLanguage extends Language {
    private final String _name;
    private final boolean _inlineVariables;
    private final AstOptimizationStep _abortBeforeStep;

    public BytecodeAstLanguage() {
        this("Bytecode AST", true, AstOptimizationStep.None);
    }

    private BytecodeAstLanguage(final String name, final boolean inlineVariables, final AstOptimizationStep abortBeforeStep) {
        _name = name;
        _inlineVariables = inlineVariables;
        _abortBeforeStep = abortBeforeStep;
    }

    @Override
    public String getName() {
        return _name;
    }

    @Override
    public String getFileExtension() {
        return ".jvm";
    }

    @Override
    public TypeDecompilationResults decompileType(final TypeDefinition type, final ITextOutput output, final DecompilationOptions options) {
        writeTypeHeader(type, output);

        output.writeLine(" {");
        output.indent();

        try {
            boolean first = true;

            for (final MethodDefinition method : type.getDeclaredMethods()) {
                if (!first) {
                    output.writeLine();
                }
                else {
                    first = false;
                }

                decompileMethod(method, output, options);
            }

            if (!options.getSettings().getExcludeNestedTypes()) {
                for (final TypeDefinition innerType : type.getDeclaredTypes()) {
                    output.writeLine();
                    decompileType(innerType, output, options);
                }
            }
        }
        finally {
            output.unindent();
            output.writeLine("}");
        }
        
        return new TypeDecompilationResults( null /*no line number mapping*/);        
    }

    @Override
    @SuppressWarnings("ConstantConditions")
    public void decompileMethod(final MethodDefinition method, final ITextOutput output, final DecompilationOptions options) {
        VerifyArgument.notNull(method, "method");
        VerifyArgument.notNull(output, "output");
        VerifyArgument.notNull(options, "options");

        writeMethodHeader(method, output);

        final MethodBody body = method.getBody();

        if (body == null) {
            output.writeDelimiter(";");
            output.writeLine();
            return;
        }

        final DecompilerContext context = new DecompilerContext();

        context.setCurrentMethod(method);
        context.setCurrentType(method.getDeclaringType());

        final Block methodAst = new Block();

        output.writeLine(" {");
        output.indent();

        try {
            methodAst.getBody().addAll(AstBuilder.build(body, _inlineVariables, context));

            if (_abortBeforeStep != null) {
                AstOptimizer.optimize(context, methodAst, _abortBeforeStep);
            }

            final Set<Variable> allVariables = new LinkedHashSet<>();

            for (final Expression e : methodAst.getSelfAndChildrenRecursive(Expression.class)) {
                final Object operand = e.getOperand();

                if (operand instanceof Variable && !((Variable) operand).isParameter()) {
                    allVariables.add((Variable) operand);
                }
            }

            if (!allVariables.isEmpty()) {
                for (final Variable variable : allVariables) {
                    output.writeDefinition(variable.getName(), variable);

                    final TypeReference type = variable.getType();

                    if (type != null) {
                        output.write(" : ");
                        DecompilerHelpers.writeType(output, type, NameSyntax.SHORT_TYPE_NAME);
                    }

                    if (variable.isGenerated()) {
                        output.write(" [generated]");
                    }

                    output.writeLine();
                }

                output.writeLine();
            }

            methodAst.writeTo(output);
        }
        catch (Throwable t) {
            writeError(output, t);
        }
        finally {
            output.unindent();
            output.writeLine("}");
        }
    }

    private static void writeError(final ITextOutput output, final Throwable t) {
        final List<String> lines = StringUtilities.split(
            ExceptionUtilities.getStackTraceString(t),
            true,
            '\r',
            '\n'
        );

        for (final String line : lines) {
            output.writeComment("// " + line.replace("\t", "    "));
            output.writeLine();
        }
    }

    private void writeTypeHeader(final TypeDefinition type, final ITextOutput output) {
        long flags = type.getFlags() & (Flags.ClassFlags | Flags.STATIC | Flags.FINAL);

        if (type.isInterface()) {
            flags &= ~Flags.ABSTRACT;
        }
        else if (type.isEnum()) {
            flags &= Flags.AccessFlags;
        }

        for (final Modifier modifier : Flags.asModifierSet(flags)) {
            output.writeKeyword(modifier.toString());
            output.write(' ');
        }

        if (type.isInterface()) {
            if (type.isAnnotation()) {
                output.writeKeyword("@interface");
            }
            else {
                output.writeKeyword("interface");
            }
        }
        else if (type.isEnum()) {
            output.writeKeyword("enum");
        }
        else {
            output.writeKeyword("class");
        }

        output.write(' ');

        DecompilerHelpers.writeType(output, type, NameSyntax.TYPE_NAME, true);
    }

    private void writeMethodHeader(final MethodDefinition method, final ITextOutput output) {
        if (method.isTypeInitializer()) {
            output.writeKeyword("static");
            return;
        }

        if (!method.getDeclaringType().isInterface()) {
            for (final Modifier modifier : Flags.asModifierSet(method.getFlags() & Flags.MethodFlags)) {
                output.writeKeyword(modifier.toString());
                output.write(' ');
            }
        }

        if (!method.isTypeInitializer()) {
            DecompilerHelpers.writeType(output, method.getReturnType(), NameSyntax.TYPE_NAME);
            output.write(' ');

            if (method.isConstructor()) {
                output.writeReference(method.getDeclaringType().getName(), method.getDeclaringType());
            }
            else {
                output.writeReference(method.getName(), method);
            }

            output.write("(");

            final List<ParameterDefinition> parameters = method.getParameters();

            for (int i = 0; i < parameters.size(); i++) {
                final ParameterDefinition parameter = parameters.get(i);

                if (i != 0) {
                    output.write(", ");
                }

                DecompilerHelpers.writeType(output, parameter.getParameterType(), NameSyntax.TYPE_NAME);
                output.write(' ');
                output.writeReference(parameter.getName(), parameter);
            }

            output.write(")");
        }
    }

    @Override
    public String typeToString(final TypeReference type, final boolean includePackage) {
        final ITextOutput output = new PlainTextOutput();
        DecompilerHelpers.writeType(output, type, includePackage ? NameSyntax.TYPE_NAME : NameSyntax.SHORT_TYPE_NAME);
        return output.toString();
    }

    public static List<BytecodeAstLanguage> getDebugLanguages() {
        final AstOptimizationStep[] steps = AstOptimizationStep.values();
        final BytecodeAstLanguage[] languages = new BytecodeAstLanguage[steps.length];

        languages[0] = new BytecodeAstLanguage("Bytecode AST (Unoptimized)", false, steps[0]);

        String nextName = "Bytecode AST (Variable Splitting)";

        for (int i = 1; i < languages.length; i++) {
            languages[i] = new BytecodeAstLanguage(nextName, true, steps[i - 1]);
            nextName = "Bytecode AST (After " + steps[i - 1].name() + ")";
        }

        return ArrayUtilities.asUnmodifiableList(languages);
    }
}
