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

import com.google.common.base.Preconditions;
import com.intellij.lang.ASTNode;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jf.dexlib2.Format;
import org.jf.dexlib2.Opcode;
import org.jf.dexlib2.Opcodes;
import org.jf.dexlib2.analysis.AnalyzedInstruction;
import org.jf.dexlib2.analysis.MethodAnalyzer;
import org.jf.smalidea.SmaliTokens;
import org.jf.smalidea.psi.SmaliCompositeElementFactory;
import org.jf.smalidea.psi.SmaliElementTypes;

import java.util.Arrays;
import java.util.List;

public class SmaliInstruction extends SmaliCompositeElement {
    private static final int NO_OFFSET = -1;

    @Nullable private Opcode opcode;
    private int offset = NO_OFFSET;

    public static final SmaliCompositeElementFactory FACTORY = new SmaliCompositeElementFactory() {
        @Override public SmaliCompositeElement createElement() {
            return new SmaliInstruction();
        }
    };

    public SmaliInstruction() {
        super(SmaliElementTypes.INSTRUCTION);
    }

    @NotNull public SmaliMethod getParentMethod() {
        SmaliMethod smaliMethod = findAncestorByClass(SmaliMethod.class);
        assert smaliMethod != null;
        return smaliMethod;
    }

    @NotNull public Opcode getOpcode() {
        if (opcode == null) {
            ASTNode instructionNode = findChildByType(SmaliTokens.INSTRUCTION_TOKENS);
            // this should be impossible, based on the parser definition
            assert instructionNode != null;

            // TODO: put a project level Opcodes instance with the appropriate api level somewhere
            opcode = Opcodes.getDefault().getOpcodeByName(instructionNode.getText());
            if (opcode == null) {
                if (instructionNode.getText().equals(".packed-switch")) {
                    return Opcode.PACKED_SWITCH_PAYLOAD;
                }
                if (instructionNode.getText().equals(".sparse-switch")) {
                    return Opcode.SPARSE_SWITCH_PAYLOAD;
                }
                if (instructionNode.getText().equals(".array-data")) {
                    return Opcode.ARRAY_PAYLOAD;
                }
                assert false;
            }
        }
        return opcode;
    }

    public int getOffset() {
        // TODO: don't calculate this recursively. ugh!
        if (offset == NO_OFFSET) {
            SmaliInstruction previousInstruction = findPrevSiblingByClass(SmaliInstruction.class);
            if (previousInstruction == null) {
                offset = 0;
            } else {
                offset = previousInstruction.getOffset() + previousInstruction.getInstructionSize();
            }
        }
        return offset;
    }

    public int getRegister(int registerIndex) {
        Preconditions.checkArgument(registerIndex >= 0);

        List<ASTNode> registers = findChildrenByType(SmaliElementTypes.REGISTER_REFERENCE);
        if (registerIndex >= registers.size()) {
            return -1;
        }

        SmaliRegisterReference registerReference = (SmaliRegisterReference)registers.get(registerIndex);
        return registerReference.getRegisterNumber();
    }

    @Nullable
    public SmaliLabelReference getTarget() {
        return findChildByClass(SmaliLabelReference.class);
    }

    public int getRegisterCount() {
        return findChildrenByType(SmaliElementTypes.REGISTER_REFERENCE).size();
    }

    @Nullable
    public SmaliLiteral getLiteral() {
        return findChildByClass(SmaliLiteral.class);
    }

    @Nullable
    public SmaliTypeElement getTypeReference() {
        return findChildByClass(SmaliTypeElement.class);
    }

    @Nullable
    public SmaliFieldReference getFieldReference() {
        return findChildByClass(SmaliFieldReference.class);
    }

    @Nullable
    public SmaliMethodReference getMethodReference() {
        return findChildByClass(SmaliMethodReference.class);
    }

    @Nullable
    public SmaliLiteral getPackedSwitchStartKey() {
        return findChildByClass(SmaliLiteral.class);
    }

    @NotNull
    public List<SmaliPackedSwitchElement> getPackedSwitchElements() {
        return Arrays.asList(findChildrenByClass(SmaliPackedSwitchElement.class));
    }

    @NotNull
    public List<SmaliSparseSwitchElement> getSparseSwitchElements() {
        return Arrays.asList(findChildrenByClass(SmaliSparseSwitchElement.class));
    }

    @Nullable
    public SmaliLiteral getArrayDataWidth() {
        return findChildByClass(SmaliLiteral.class);
    }

    @NotNull
    public List<SmaliArrayDataElement> getArrayDataElements() {
        return Arrays.asList(findChildrenByClass(SmaliArrayDataElement.class));
    }

    public int getInstructionSize() {
        Opcode opcode = getOpcode();
        if (!opcode.format.isPayloadFormat) {
            return opcode.format.size;
        } else if (opcode.format == Format.ArrayPayload) {
            int elementWidth = (int)getArrayDataWidth().getIntegralValue();
            int elementCount = getArrayDataElements().size();

            return 8 + (elementWidth * elementCount + 1);
        } else if (opcode.format == Format.PackedSwitchPayload) {
            return 8 + getPackedSwitchElements().size() * 4;
        } else if (opcode.format == Format.SparseSwitchPayload) {
            return 2 + getSparseSwitchElements().size() * 4;
        }
        assert false;
        throw new RuntimeException();
    }

    private AnalyzedInstruction analyzedInstruction = null;

    @Nullable
    private AnalyzedInstruction getAnalyzedInstructionFromMethod() {
        SmaliMethod method = getParentMethod();

        MethodAnalyzer analyzer = method.getMethodAnalyzer();
        if (analyzer == null) {
            return null;
        }

        int thisOffset = this.getOffset() / 2;
        int codeOffset = 0;

        for (AnalyzedInstruction instruction: analyzer.getAnalyzedInstructions()) {
            if (codeOffset == thisOffset) {
                return instruction;
            }
            assert codeOffset < thisOffset;

            codeOffset += instruction.getOriginalInstruction().getCodeUnits();
        }
        assert false;
        return null;
    }

    @Nullable
    public AnalyzedInstruction getAnalyzedInstruction() {
        if (analyzedInstruction == null) {
            analyzedInstruction = getAnalyzedInstructionFromMethod();
        }
        return analyzedInstruction;
    }

    @Override public void clearCaches() {
        super.clearCaches();
        analyzedInstruction = null;
    }
}
