/*
 * DecompilerHelpers.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;

import com.strobel.assembler.ir.ExceptionHandler;
import com.strobel.assembler.ir.Frame;
import com.strobel.assembler.ir.FrameType;
import com.strobel.assembler.ir.FrameValue;
import com.strobel.assembler.ir.FrameValueType;
import com.strobel.assembler.ir.Instruction;
import com.strobel.assembler.ir.InstructionBlock;
import com.strobel.assembler.metadata.*;
import com.strobel.core.StringUtilities;
import com.strobel.core.VerifyArgument;
import com.strobel.decompiler.ast.Variable;

import java.util.List;
import java.util.Stack;

import static java.lang.String.format;

public final class DecompilerHelpers {
    public static void writeType(final ITextOutput writer, final TypeReference type) {
        writeType(writer, type, NameSyntax.SIGNATURE);
    }

    public static void writeGenericSignature(final ITextOutput writer, final TypeReference type) {
        formatGenericSignature(writer, type, new Stack<TypeReference>());
    }

    public static void writeType(final ITextOutput writer, final TypeReference type, final NameSyntax syntax) {
        VerifyArgument.notNull(type, "type");
        VerifyArgument.notNull(writer, "writer");
        VerifyArgument.notNull(syntax, "syntax");

        formatType(writer, type, syntax, type.isDefinition(), new Stack<TypeReference>());
    }

    public static void writeType(
        final ITextOutput writer,
        final TypeReference type,
        final NameSyntax syntax,
        final boolean isDefinition) {

        VerifyArgument.notNull(type, "type");
        VerifyArgument.notNull(writer, "writer");
        VerifyArgument.notNull(syntax, "syntax");

        formatType(writer, type, syntax, isDefinition, new Stack<TypeReference>());
    }

    public static void writeMethod(final ITextOutput writer, final MethodReference method) {
        VerifyArgument.notNull(method, "method");
        VerifyArgument.notNull(writer, "writer");

        final Stack<TypeReference> typeStack = new Stack<>();

        formatType(writer, method.getDeclaringType(), NameSyntax.DESCRIPTOR, false, typeStack);
        writer.writeDelimiter(".");
        writer.writeReference(method.getName(), method);
        writer.writeDelimiter(":");
        formatMethodSignature(writer, method, typeStack);
    }

    public static void writeMethodSignature(final ITextOutput writer, final IMethodSignature signature) {
        VerifyArgument.notNull(signature, "signature");
        VerifyArgument.notNull(writer, "writer");

        final Stack<TypeReference> typeStack = new Stack<>();

        formatMethodSignature(writer, signature, typeStack);
    }

    public static void writeField(final ITextOutput writer, final FieldReference field) {
        VerifyArgument.notNull(field, "field");
        VerifyArgument.notNull(writer, "writer");

        final Stack<TypeReference> typeStack = new Stack<>();

        formatType(writer, field.getDeclaringType(), NameSyntax.DESCRIPTOR, false, typeStack);
        writer.writeDelimiter(".");
        writer.writeReference(field.getName(), field);
        writer.writeDelimiter(":");
        formatType(writer, field.getFieldType(), NameSyntax.SIGNATURE, false, typeStack);
    }

    public static void writeOperand(final ITextOutput writer, final Object operand) {
        writeOperand(writer, operand, false);
    }

    public static void writeOperand(final ITextOutput writer, final Object operand, final boolean isUnicodeSupported) {
        VerifyArgument.notNull(writer, "writer");
        VerifyArgument.notNull(operand, "operand");

        if (operand instanceof Instruction) {
            final Instruction targetInstruction = (Instruction) operand;
            writeOffsetReference(writer, targetInstruction);
            return;
        }

        if (operand instanceof Instruction[]) {
            final Instruction[] targetInstructions = (Instruction[]) operand;
            writeLabelList(writer, targetInstructions);
            return;
        }

        if (operand instanceof SwitchInfo) {
            final SwitchInfo switchInfo = (SwitchInfo) operand;

            writer.write('[');
            writeOffsetReference(writer, switchInfo.getDefaultTarget());

            for (final Instruction target : switchInfo.getTargets()) {
                writer.write(", ");
                writeOffsetReference(writer, target);
            }

            writer.write(']');
            return;
        }

        if (operand instanceof VariableReference) {
            final VariableReference variable = (VariableReference) operand;

            if (variable.hasName()) {
                writer.writeReference(escapeIdentifier(variable.getName()), variable);
            }
            else {
                writer.writeReference("$" + String.valueOf(variable.getSlot()), variable);
            }

            return;
        }

        if (operand instanceof ParameterReference) {
            final ParameterReference parameter = (ParameterReference) operand;
            final String parameterName = parameter.getName();

            if (StringUtilities.isNullOrEmpty(parameterName)) {
                writer.writeReference(String.valueOf(parameter.getPosition()), parameter);
            }
            else {
                writer.writeReference(escapeIdentifier(parameterName), parameter);
            }

            return;
        }

        if (operand instanceof Variable) {
            final Variable variable = (Variable) operand;

            if (variable.isParameter()) {
                writer.writeReference(variable.getName(), variable.getOriginalParameter());
            }
            else {
                writer.writeReference(variable.getName(), variable.getOriginalVariable());
            }

            return;
        }

        if (operand instanceof MethodReference) {
            writeMethod(writer, (MethodReference) operand);
            return;
        }

        if (operand instanceof TypeReference) {
            writeType(writer, (TypeReference) operand, NameSyntax.TYPE_NAME);
            writer.write('.');
            writer.writeKeyword("class");
            return;
        }

        if (operand instanceof FieldReference) {
            writeField(writer, (FieldReference) operand);
            return;
        }

//        if (operand instanceof String) {
//            writer.writeLiteral(JavaOutputVisitor.convertString((String) operand, true));
//            return;
//        }
//
//        if (operand instanceof Character) {
//            writer.writeLiteral(String.valueOf((int) ((Character) operand).charValue()));
//            return;
//        }
//
//        if (operand instanceof Boolean) {
//            writer.writeKeyword(Boolean.TRUE.equals(operand) ? "true" : "false");
//            return;
//        }
//
//        if (operand instanceof Number) {
//            writer.writeLiteral(operand);
//            return;
//        }

        if (operand instanceof DynamicCallSite) {
            writeDynamicCallSite(writer, (DynamicCallSite) operand);
            return;
        }

        writePrimitiveValue(writer, operand);
    }

    public static void writeDynamicCallSite(final ITextOutput output, final DynamicCallSite operand) {
        output.writeReference(operand.getMethodName(), operand.getMethodType());
        output.writeDelimiter(":");
        writeMethodSignature(output, operand.getMethodType());
    }

    public static String offsetToString(final int offset) {
        return format("#%1$04d", offset);
    }

    public static void writeExceptionHandler(final ITextOutput output, final ExceptionHandler handler) {
        VerifyArgument.notNull(output, "output");
        VerifyArgument.notNull(handler, "handler");

        output.write("Try ");
        writeOffsetReference(output, handler.getTryBlock().getFirstInstruction());
        output.write(" - ");
        writeEndOffsetReference(output, handler.getTryBlock().getLastInstruction());
        output.write(' ');
        output.write(String.valueOf(handler.getHandlerType()));

        final TypeReference catchType = handler.getCatchType();

        if (catchType != null) {
            output.write(' ');
            writeType(output, catchType);
        }

        final InstructionBlock handlerBlock = handler.getHandlerBlock();

        output.write(' ');
        writeOffsetReference(output, handlerBlock.getFirstInstruction());

        if (handlerBlock.getLastInstruction() != null) {
            output.write(" - ");
            writeEndOffsetReference(output, handlerBlock.getLastInstruction());
        }
    }

    public static void writeInstruction(final ITextOutput writer, final Instruction instruction) {
        VerifyArgument.notNull(writer, "writer");
        VerifyArgument.notNull(instruction, "instruction");

        writer.writeDefinition(offsetToString(instruction.getOffset()), instruction);
        writer.write(": ");
        writer.writeReference(instruction.getOpCode().name(), instruction.getOpCode());

        if (instruction.hasOperand()) {
            writer.write(' ');
            writeOperandList(writer, instruction);
        }
    }

    public static void writeOffsetReference(final ITextOutput writer, final Instruction instruction) {
        VerifyArgument.notNull(writer, "writer");

        writer.writeLabel(offsetToString(instruction.getOffset()));
    }

    public static void writeEndOffsetReference(final ITextOutput writer, final Instruction instruction) {
        VerifyArgument.notNull(writer, "writer");

        writer.writeLabel(offsetToString(instruction.getEndOffset()));
    }

    public static String escapeIdentifier(final String name) {
        VerifyArgument.notNull(name, "name");

        StringBuilder sb = null;

        for (int i = 0, n = name.length(); i < n; i++) {
            final char ch = name.charAt(i);

            if (i == 0) {
                if (Character.isJavaIdentifierStart(ch)) {
                    continue;
                }
                sb = new StringBuilder(name.length() * 2);
                sb.append(format("\\u%1$04x", (int) ch));
            }
            else if (Character.isJavaIdentifierPart(ch)) {
                if (sb != null) {
                    sb.append(ch);
                }
            }
            else {
                if (sb == null) {
                    sb = new StringBuilder(name.length() * 2);
                }
                sb.append(format("\\u%1$04x", (int) ch));
            }
        }

        if (sb != null) {
            return sb.toString();
        }

        return name;
    }

    public static void writeFrame(final ITextOutput writer, final Frame frame) {
        VerifyArgument.notNull(writer, "writer");
        VerifyArgument.notNull(frame, "frame");

        final FrameType frameType = frame.getFrameType();

        writer.writeLiteral(String.valueOf(frameType));

        final List<FrameValue> localValues = frame.getLocalValues();
        final List<FrameValue> stackValues = frame.getStackValues();

        if (!localValues.isEmpty()) {
            writer.writeLine();
            writer.indent();
            writer.write("Locals: ");
            writer.writeDelimiter("[");

            for (int i = 0; i < localValues.size(); i++) {
                final FrameValue value = localValues.get(i);

                if (i != 0) {
                    writer.writeDelimiter(", ");
                }

                if (value.getType() == FrameValueType.Reference) {
                    writer.writeLiteral("Reference");
                    writer.writeDelimiter("(");
                    writeType(writer, (TypeReference) value.getParameter(), NameSyntax.SIGNATURE);
                    writer.writeDelimiter(")");
                }
                else {
                    writer.writeLiteral(String.valueOf(value.getType()));
                }
            }

            writer.writeDelimiter("]");
            writer.unindent();
        }

        if (!stackValues.isEmpty()) {
            writer.writeLine();
            writer.indent();
            writer.write("Stack: ");
            writer.writeDelimiter("[");

            for (int i = 0; i < stackValues.size(); i++) {
                final FrameValue value = stackValues.get(i);

                if (i != 0) {
                    writer.writeDelimiter(", ");
                }

                if (value.getType() == FrameValueType.Reference) {
                    writer.writeLiteral("Reference");
                    writer.writeDelimiter("(");
                    writeType(writer, (TypeReference) value.getParameter(), NameSyntax.SIGNATURE);
                    writer.writeDelimiter(")");
                }
                else {
                    writer.writeLiteral(String.valueOf(value.getType()));
                }
            }

            writer.writeDelimiter("]");
            writer.unindent();
        }
    }

    private static void writeLabelList(final ITextOutput writer, final Instruction[] instructions) {
        writer.write('(');

        for (int i = 0; i < instructions.length; i++) {
            if (i != 0) {
                writer.write(", ");
            }
            writeOffsetReference(writer, instructions[i]);
        }

        writer.write(')');
    }

    private static void writeOperandList(final ITextOutput writer, final Instruction instruction) {
        for (int i = 0, n = instruction.getOperandCount(); i < n; i++) {
            if (i != 0) {
                writer.write(", ");
            }
            writeOperand(writer, instruction.getOperand(i));
        }
    }

    private static void formatMethodSignature(
        final ITextOutput writer,
        final IMethodSignature signature,
        final Stack<TypeReference> typeStack) {

        if (signature.isGenericDefinition()) {
            final List<GenericParameter> genericParameters = signature.getGenericParameters();
            final int count = genericParameters.size();

            if (count > 0) {
                writer.writeDelimiter("<");
                for (int i = 0; i < count; ++i) {
                    formatGenericSignature(writer, genericParameters.get(i), typeStack);
                }
                writer.writeDelimiter(">");
            }
        }

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

        writer.writeDelimiter("(");

        for (int i = 0, n = parameters.size(); i < n; ++i) {
            final ParameterDefinition p = parameters.get(i);
            if (p.isSynthetic()) {
                continue;
            }
            formatType(writer, p.getParameterType(), NameSyntax.SIGNATURE, false, typeStack);
        }

        writer.writeDelimiter(")");

        formatType(writer, signature.getReturnType(), NameSyntax.SIGNATURE, false, typeStack);
    }

    @SuppressWarnings("ConstantConditions")
    private static void formatType(
        final ITextOutput writer,
        final TypeReference type,
        final NameSyntax syntax,
        final boolean isDefinition,
        final Stack<TypeReference> stack) {

        if (type.isGenericParameter()) {
            switch (syntax) {
                case SIGNATURE:
                case ERASED_SIGNATURE:
                case DESCRIPTOR: {
                    writer.writeDelimiter("T");
                    writer.writeReference(type.getSimpleName(), type);
                    writer.writeDelimiter(";");
                    return;
                }

                default: {
                    writer.writeReference(type.getName(), type);

                    if (isDefinition &&
                        type.hasExtendsBound() &&
                        !stack.contains(type.getExtendsBound()) &&
                        !BuiltinTypes.Object.equals(type.getExtendsBound())) {

                        writer.writeKeyword(" extends ");
                        stack.push(type);

                        try {
                            formatType(writer, type.getExtendsBound(), syntax, false, stack);
                        }
                        finally {
                            stack.pop();
                        }
                    }

                    return;
                }
            }
        }

        if (type.isWildcardType()) {
            switch (syntax) {
                case DESCRIPTOR: {
                    formatType(writer, type.getExtendsBound(), syntax, false, stack);
                    return;
                }

                case SIGNATURE:
                case ERASED_SIGNATURE: {
                    if (type.hasSuperBound()) {
                        writer.write('-');
                        formatType(writer, type.getSuperBound(), syntax, false, stack);
                    }
                    else if (type.hasExtendsBound()) {
                        writer.write('+');
                        formatType(writer, type.getExtendsBound(), syntax, false, stack);
                    }
                    else {
                        writer.write('*');
                    }
                    return;
                }

                default: {
                    writer.write("?");

                    if (type.hasSuperBound()) {
                        writer.writeKeyword(" super ");
                        formatType(writer, type.getSuperBound(), syntax, false, stack);
                    }
                    else if (type.hasExtendsBound()) {
                        writer.writeKeyword(" extends ");
                        formatType(writer, type.getExtendsBound(), syntax, false, stack);
                    }

                    return;
                }
            }
        }

        if (type instanceof CompoundTypeReference) {
            final CompoundTypeReference compoundType = (CompoundTypeReference) type;
            final TypeReference baseType = compoundType.getBaseType();
            final List<TypeReference> interfaces = compoundType.getInterfaces();

            switch (syntax) {
                case SIGNATURE: {
                    if (baseType != null) {
                        formatType(writer, baseType, syntax, false, stack);
                    }

                    for (final TypeReference interfaceType : interfaces) {
                        writer.writeDelimiter(":");
                        formatType(writer, interfaceType, syntax, false, stack);
                    }

                    break;
                }

                case ERASED_SIGNATURE:
                case DESCRIPTOR: {
                    final TypeReference erasedType;

                    if (baseType != null) {
                        erasedType = baseType;
                    }
                    else if (!interfaces.isEmpty()) {
                        erasedType = interfaces.get(0);
                    }
                    else {
                        erasedType = BuiltinTypes.Object;
                    }

                    formatType(writer, erasedType, syntax, false, stack);
                    break;
                }

                case TYPE_NAME:
                case SHORT_TYPE_NAME: {
                    boolean first = true;

                    if (baseType != null) {
                        formatType(writer, baseType, syntax, false, stack);
                        first = false;
                    }

                    for (final TypeReference interfaceType : interfaces) {
                        if (!first) {
                            writer.writeDelimiter(" & ");
                        }

                        formatType(writer, interfaceType, syntax, false, stack);
                        first = false;
                    }

                    break;
                }
            }

            return;
        }

        if (type.isArray()) {
            switch (syntax) {
                case SIGNATURE:
                case ERASED_SIGNATURE:
                case DESCRIPTOR: {
                    writer.writeDelimiter("[");
                    formatType(writer, type.getElementType(), syntax, false, stack);
                    break;
                }

                case TYPE_NAME:
                case SHORT_TYPE_NAME: {
                    formatType(writer, type.getElementType(), syntax, false, stack);
                    writer.writeDelimiter("[]");
                }
            }

            return;
        }

        stack.push(type);

        final TypeDefinition resolvedType = type.resolve();
        final TypeReference nameSource = resolvedType != null ? resolvedType : type;

        try {
            final String name;

            switch (syntax) {
                case TYPE_NAME:
                    name = nameSource.getFullName();
                    break;
                case SHORT_TYPE_NAME:
                    name = nameSource.getSimpleName();
                    break;
                case DESCRIPTOR:
                    name = nameSource.getInternalName();
                    break;
                default:
                    if (nameSource.isPrimitive()) {
                        name = nameSource.getInternalName();
                    }
                    else {
                        writer.writeDelimiter("L");
                        name = nameSource.getInternalName();
                    }
                    break;
            }

            if (type.isPrimitive() && (syntax == NameSyntax.TYPE_NAME || syntax == NameSyntax.SHORT_TYPE_NAME)) {
                writer.writeKeyword(name);
            }
            else if (isDefinition) {
                writer.writeDefinition(name, type);
            }
            else {
                writer.writeReference(name, type);
            }

            if (type.isGenericType() &&
                syntax != NameSyntax.DESCRIPTOR &&
                syntax != NameSyntax.ERASED_SIGNATURE) {

                stack.push(type);

                try {
                    final List<? extends TypeReference> typeArguments;

                    if (type instanceof IGenericInstance) {
                        typeArguments = ((IGenericInstance) type).getTypeArguments();
                    }
                    else {
                        typeArguments = type.getGenericParameters();
                    }

                    final int count = typeArguments.size();

                    if (count > 0) {
                        writer.writeDelimiter("<");
                        for (int i = 0; i < count; ++i) {
                            if (syntax != NameSyntax.SIGNATURE) {
                                if (i != 0) {
                                    writer.writeDelimiter(", ");
                                }
                            }

                            final TypeReference typeArgument = typeArguments.get(i);

                            formatType(writer, typeArgument, syntax, false, stack);
                        }
                        writer.writeDelimiter(">");
                    }
                }
                finally {
                    stack.pop();
                }
            }

            if (!type.isPrimitive() && (syntax == NameSyntax.SIGNATURE || syntax == NameSyntax.ERASED_SIGNATURE)) {
                writer.writeDelimiter(";");
            }
        }
        finally {
            stack.pop();
        }
    }

    private static void formatGenericSignature(
        final ITextOutput writer,
        final TypeReference type,
        final Stack<TypeReference> stack) {

        if (type.isGenericParameter()) {
            final TypeReference extendsBound = type.getExtendsBound();
            final TypeDefinition resolvedBound = extendsBound.resolve();

            writer.writeDefinition(type.getName(), type);

            if (resolvedBound != null && resolvedBound.isInterface()) {
                writer.writeDelimiter(":");
            }

            writer.writeDelimiter(":");

            formatType(writer, extendsBound, NameSyntax.SIGNATURE, false, stack);

            return;
        }

        if (type.isGenericType()) {
            final List<? extends TypeReference> typeArguments;

            if (type instanceof IGenericInstance) {
                typeArguments = ((IGenericInstance) type).getTypeArguments();
            }
            else {
                typeArguments = type.getGenericParameters();
            }

            final int count = typeArguments.size();

            if (count > 0) {
                writer.writeDelimiter("<");
                //noinspection ForLoopReplaceableByForEach
                for (int i = 0; i < count; ++i) {
                    formatGenericSignature(writer, typeArguments.get(i), stack);
                }
                writer.writeDelimiter(">");
            }
        }

        final TypeDefinition definition = type.resolve();

        if (definition == null) {
            return;
        }

        final TypeReference baseType = definition.getBaseType();
        final List<TypeReference> interfaces = definition.getExplicitInterfaces();

        if (baseType == null) {
            formatType(writer, BuiltinTypes.Object, NameSyntax.SIGNATURE, false, stack);
        }
        else {
            formatType(writer, baseType, NameSyntax.SIGNATURE, false, stack);
        }

        for (final TypeReference interfaceType : interfaces) {
            formatType(writer, interfaceType, NameSyntax.SIGNATURE, false, stack);
        }
    }

    public static void writePrimitiveValue(final ITextOutput output, final Object value) {
        if (value == null) {
            output.writeKeyword("null");
            return;
        }

        if (value instanceof Boolean) {
            if ((Boolean) value) {
                output.writeKeyword("true");
            }
            else {
                output.writeKeyword("false");
            }
            return;
        }

        if (value instanceof String) {
            output.writeTextLiteral(StringUtilities.escape(value.toString(), true, true));
        }
        else if (value instanceof Character) {
            output.writeTextLiteral(StringUtilities.escape((char) value, true, true));
        }
        else if (value instanceof Float) {
            final float f = (Float) value;
            if (Float.isInfinite(f) || Float.isNaN(f)) {
                output.writeReference("Float", MetadataSystem.instance().lookupType("java/lang/Float"));
                output.writeDelimiter(".");
                if (f == Float.POSITIVE_INFINITY) {
                    output.write("POSITIVE_INFINITY");
                }
                else if (f == Float.NEGATIVE_INFINITY) {
                    output.write("NEGATIVE_INFINITY");
                }
                else {
                    output.write("NaN");
                }
                return;
            }
            output.writeLiteral(Float.toString(f) + "f");
        }
        else if (value instanceof Double) {
            final double d = (Double) value;
            if (Double.isInfinite(d) || Double.isNaN(d)) {
                final TypeReference doubleType = MetadataSystem.instance().lookupType("java/lang/Double");
                output.writeReference("Double", doubleType);
                output.writeDelimiter(".");
                if (d == Double.POSITIVE_INFINITY) {
                    output.write("POSITIVE_INFINITY");
                }
                else if (d == Double.NEGATIVE_INFINITY) {
                    output.write("NEGATIVE_INFINITY");
                }
                else {
                    output.write("NaN");
                }
                return;
            }

            String number = Double.toString(d);

            if (number.indexOf('.') < 0 && number.indexOf('E') < 0) {
                number += "d";
            }

            output.writeLiteral(number);
        }
        else if (value instanceof Long) {
            output.writeLiteral(String.valueOf(value) + "L");
        }
        else {
            output.writeLiteral(String.valueOf(value));
        }
    }
}
