/*
 * Copyright 2014, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name of Google Inc. 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
 * OWNER 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.jf.smalidea.debugging;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.debugger.SourcePosition;
import com.intellij.debugger.engine.evaluation.*;
import com.intellij.debugger.engine.evaluation.expression.EvaluatorBuilder;
import com.intellij.debugger.engine.evaluation.expression.ExpressionEvaluator;
import com.intellij.debugger.engine.jdi.StackFrameProxy;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Key;
import com.intellij.psi.JavaCodeFragment;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.util.PsiMatchers;
import com.sun.jdi.*;
import com.sun.tools.jdi.LocalVariableImpl;
import com.sun.tools.jdi.LocationImpl;
import org.jf.dexlib2.analysis.AnalyzedInstruction;
import org.jf.dexlib2.analysis.RegisterType;
import org.jf.smalidea.SmaliFileType;
import org.jf.smalidea.SmaliLanguage;
import org.jf.smalidea.debugging.value.LazyValue;
import org.jf.smalidea.psi.impl.SmaliInstruction;
import org.jf.smalidea.psi.impl.SmaliMethod;
import org.jf.smalidea.util.NameUtils;
import org.jf.smalidea.util.PsiUtil;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;

public class SmaliCodeFragmentFactory extends DefaultCodeFragmentFactory {
    static final Key<List<LazyValue>> SMALI_LAZY_VALUES_KEY = Key.create("_smali_register_value_key_");

    @Override
    public JavaCodeFragment createCodeFragment(TextWithImports item, PsiElement context, Project project) {
        context = wrapContext(project, context);
        JavaCodeFragment fragment = super.createCodeFragment(item, context, project);
        List<LazyValue> lazyValues = context.getUserData(SMALI_LAZY_VALUES_KEY);
        if (lazyValues != null) {
            fragment.putUserData(SMALI_LAZY_VALUES_KEY, lazyValues);
        }
        return fragment;
    }

    @Override
    public boolean isContextAccepted(PsiElement contextElement) {
        if (contextElement == null) {
            return false;
        }
        return contextElement.getLanguage() == SmaliLanguage.INSTANCE;
    }

    @Override
    public JavaCodeFragment createPresentationCodeFragment(TextWithImports item, PsiElement context, Project project) {
        context = wrapContext(project, context);
        JavaCodeFragment fragment = super.createPresentationCodeFragment(item, context, project);
        List<LazyValue> lazyValues = context.getUserData(SMALI_LAZY_VALUES_KEY);
        if (lazyValues != null) {
            fragment.putUserData(SMALI_LAZY_VALUES_KEY, lazyValues);
        }
        return fragment;
    }

    @Override public LanguageFileType getFileType() {
        return SmaliFileType.INSTANCE;
    }

    @Override public EvaluatorBuilder getEvaluatorBuilder() {
        final EvaluatorBuilder builder = super.getEvaluatorBuilder();
        return new EvaluatorBuilder() {

            @Override
            public ExpressionEvaluator build(PsiElement codeFragment, SourcePosition position)
                    throws EvaluateException {
                return new SmaliExpressionEvaluator(codeFragment, builder.build(codeFragment, position));
            }
        };
    }

    private PsiElement wrapContext(final Project project, final PsiElement originalContext) {
        if (project.isDefault()) return originalContext;

        final List<LazyValue> lazyValues = Lists.newArrayList();

        SmaliInstruction currentInstruction = (SmaliInstruction)PsiUtil.searchBackward(originalContext,
                PsiMatchers.hasClass(SmaliInstruction.class),
                PsiMatchers.hasClass(SmaliMethod.class));

        if (currentInstruction == null) {
            currentInstruction = (SmaliInstruction)PsiUtil.searchForward(originalContext,
                    PsiMatchers.hasClass(SmaliInstruction.class),
                    PsiMatchers.hasClass(SmaliMethod.class));
            if (currentInstruction == null) {
                return originalContext;
            }
        }

        final SmaliMethod containingMethod = currentInstruction.getParentMethod();
        AnalyzedInstruction analyzedInstruction = currentInstruction.getAnalyzedInstruction();
        if (analyzedInstruction == null) {
            return originalContext;
        }

        final int firstParameterRegister = containingMethod.getRegisterCount() -
                containingMethod.getParameterRegisterCount();

        final Map<String, String> registerMap = Maps.newHashMap();
        StringBuilder variablesText = new StringBuilder();
        for (int i=0; i<containingMethod.getRegisterCount(); i++) {
            int parameterRegisterNumber = i - firstParameterRegister;

            RegisterType registerType = analyzedInstruction.getPreInstructionRegisterType(i);
            switch (registerType.category) {
                case RegisterType.UNKNOWN:
                case RegisterType.UNINIT:
                case RegisterType.CONFLICTED:
                case RegisterType.LONG_HI:
                case RegisterType.DOUBLE_HI:
                    continue;
                case RegisterType.NULL:
                case RegisterType.ONE:
                case RegisterType.INTEGER:
                    variablesText.append("int v").append(i).append(";\n");
                    registerMap.put("v" + i, "I");
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append("int p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "I");
                    }
                    break;
                case RegisterType.BOOLEAN:
                    variablesText.append("boolean v").append(i).append(";\n");
                    registerMap.put("v" + i, "Z");
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append("boolean p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "Z");
                    }
                    break;
                case RegisterType.BYTE:
                case RegisterType.POS_BYTE:
                    variablesText.append("byte v").append(i).append(";\n");
                    registerMap.put("v" + i, "B");
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append("byte p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "B");
                    }
                    break;
                case RegisterType.SHORT:
                case RegisterType.POS_SHORT:
                    variablesText.append("short v").append(i).append(";\n");
                    registerMap.put("v" + i, "S");
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append("short p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "S");
                    }
                    break;
                case RegisterType.CHAR:
                    variablesText.append("char v").append(i).append(";\n");
                    registerMap.put("v" + i, "C");
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append("char p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "C");
                    }
                    break;
                case RegisterType.FLOAT:
                    variablesText.append("float v").append(i).append(";\n");
                    registerMap.put("v" + i, "F");
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append("float p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "F");
                    }
                    break;
                case RegisterType.LONG_LO:
                    variablesText.append("long v").append(i).append(";\n");
                    registerMap.put("v" + i, "J");
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append("long p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "J");
                    }
                    break;
                case RegisterType.DOUBLE_LO:
                    variablesText.append("double v").append(i).append(";\n");
                    registerMap.put("v" + i, "D");
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append("double p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "D");
                    }
                    break;
                case RegisterType.UNINIT_REF:
                case RegisterType.UNINIT_THIS:
                case RegisterType.REFERENCE:
                    String smaliType = registerType.type.getType();
                    String javaType = NameUtils.smaliToJavaType(smaliType);
                    variablesText.append(javaType).append(" v").append(i).append(";\n");
                    registerMap.put("v" + i, smaliType);
                    if (parameterRegisterNumber >= 0) {
                        variablesText.append(javaType).append(" p").append(parameterRegisterNumber).append(";\n");
                        registerMap.put("p" + parameterRegisterNumber, "Ljava/lang/Object;");
                    }
                    break;
            }
        }
        final TextWithImportsImpl textWithImports = new TextWithImportsImpl(CodeFragmentKind.CODE_BLOCK,
                variablesText.toString(), "", getFileType());

        final JavaCodeFragment codeFragment = super.createCodeFragment(textWithImports, originalContext, project);

        codeFragment.accept(new JavaRecursiveElementVisitor() {
            @Override
            public void visitLocalVariable(final PsiLocalVariable variable) {
                final String name = variable.getName();
                if (name != null && registerMap.containsKey(name)) {
                    int registerNumber = Integer.parseInt(name.substring(1));
                    if (name.charAt(0) == 'p') {
                        registerNumber += ApplicationManager.getApplication().runReadAction(new Computable<Integer>() {
                            @Override public Integer compute() {
                                return containingMethod.getRegisterCount() -
                                        containingMethod.getParameterRegisterCount();
                            }
                        });
                    }
                    LazyValue lazyValue = LazyValue.create(containingMethod, project, registerNumber,
                            registerMap.get(name));
                    variable.putUserData(CodeFragmentFactoryContextWrapper.LABEL_VARIABLE_VALUE_KEY, lazyValue);
                    lazyValues.add(lazyValue);
                }
            }
        });

        int offset = variablesText.length() - 1;

        final PsiElement newContext = codeFragment.findElementAt(offset);
        if (newContext != null) {
            newContext.putUserData(SMALI_LAZY_VALUES_KEY, lazyValues);
            return newContext;
        }
        return originalContext;
    }

    public static Value evaluateRegister(EvaluationContext context, final SmaliMethod smaliMethod,
                                         final int registerNum, final String type) throws EvaluateException {

        final StackFrameProxy frameProxy = context.getSuspendContext().getFrameProxy();
        if (frameProxy == null) {
            return null;
        }

        VirtualMachine vm = frameProxy.getStackFrame().virtualMachine();
        Location currentLocation = frameProxy.location();
        if (currentLocation == null) {
            return null;
        }

        Method method = currentLocation.method();

        try {
            final Constructor<LocalVariableImpl> localVariableConstructor = LocalVariableImpl.class.getDeclaredConstructor(
                    VirtualMachine.class, Method.class, Integer.TYPE, Location.class, Location.class, String.class,
                    String.class, String.class);
            localVariableConstructor.setAccessible(true);

            Constructor<LocationImpl> locationConstructor = LocationImpl.class.getDeclaredConstructor(
                    VirtualMachine.class, Method.class, Long.TYPE);
            locationConstructor.setAccessible(true);

            int methodSize = 0;
            for (SmaliInstruction instruction: smaliMethod.getInstructions()) {
                methodSize += instruction.getInstructionSize();
            }
            Location endLocation = method.locationOfCodeIndex((methodSize/2) - 1);

            LocalVariable localVariable = localVariableConstructor.newInstance(vm,
                    method,
                    mapRegister(frameProxy.getStackFrame().virtualMachine(), smaliMethod, registerNum),
                    method.locationOfCodeIndex(0),
                    endLocation,
                    String.format("v%d", registerNum), type, null);

            return frameProxy.getStackFrame().getValue(localVariable);
        } catch (NoSuchMethodException e) {
            return null;
        } catch (InstantiationException e) {
            return null;
        } catch (IllegalAccessException e) {
            return null;
        } catch (InvocationTargetException e) {
            return null;
        }
    }

    private static int mapRegister(final VirtualMachine vm, final SmaliMethod smaliMethod, final int register) {
        if (vm.version().equals("1.5.0")) {
            return mapRegisterForDalvik(smaliMethod, register);
        } else {
            return mapRegisterForArt(smaliMethod, register);
        }
    }

    private static int mapRegisterForArt(final SmaliMethod smaliMethod, final int register) {
        return ApplicationManager.getApplication().runReadAction(new Computable<Integer>() {
            @Override public Integer compute() {

                int totalRegisters = smaliMethod.getRegisterCount();
                int parameterRegisters = smaliMethod.getParameterRegisterCount();

                if (smaliMethod.getModifierList().hasModifierProperty("static")) {
                    return register;
                }

                // For ART, the parameter registers are rotated to the front
                if (register >= (totalRegisters - parameterRegisters)) {
                    return register - (totalRegisters - parameterRegisters);
                }
                return register + parameterRegisters;
            }
        });
    }

    private static int mapRegisterForDalvik(final SmaliMethod smaliMethod, final int register) {
        return ApplicationManager.getApplication().runReadAction(new Computable<Integer>() {
            @Override public Integer compute() {
                if (smaliMethod.getModifierList().hasModifierProperty("static")) {
                    return register;
                }

                int totalRegisters = smaliMethod.getRegisterCount();
                int parameterRegisters = smaliMethod.getParameterRegisterCount();

                // For dalvik, p0 is mapped to register 1, and register 0 is mapped to register 1000
                if (register == (totalRegisters - parameterRegisters)) {
                    return 0;
                }
                if (register == 0) {
                    return 1000;
                }
                return register;
            }
        });
    }
}

