/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jetbrains.java.decompiler.struct;

import org.jetbrains.java.decompiler.code.*;
import org.jetbrains.java.decompiler.struct.attr.StructGeneralAttribute;
import org.jetbrains.java.decompiler.struct.consts.ConstantPool;
import org.jetbrains.java.decompiler.util.DataInputFullStream;
import org.jetbrains.java.decompiler.util.VBStyleCollection;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static org.jetbrains.java.decompiler.code.CodeConstants.*;

/*
  method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
  }
*/
public class StructMethod extends StructMember {

  private static final int[] opr_iconst = {-1, 0, 1, 2, 3, 4, 5};
  private static final int[] opr_loadstore = {0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3};
  private static final int[] opcs_load = {opc_iload, opc_lload, opc_fload, opc_dload, opc_aload};
  private static final int[] opcs_store = {opc_istore, opc_lstore, opc_fstore, opc_dstore, opc_astore};

  private final StructClass classStruct;
  private final String name;
  private final String descriptor;

  private boolean containsCode = false;
  private int localVariables = 0;
  private int codeLength = 0;
  private int codeFullLength = 0;
  private InstructionSequence seq;
  private boolean expanded = false;
  private VBStyleCollection<StructGeneralAttribute, String> codeAttributes;

  public StructMethod(DataInputFullStream in, StructClass clStruct) throws IOException {
    classStruct = clStruct;

    accessFlags = in.readUnsignedShort();
    int nameIndex = in.readUnsignedShort();
    int descriptorIndex = in.readUnsignedShort();

    ConstantPool pool = clStruct.getPool();
    String[] values = pool.getClassElement(ConstantPool.METHOD, clStruct.qualifiedName, nameIndex, descriptorIndex);
    name = values[0];
    descriptor = values[1];

    attributes = readAttributes(in, pool);
    if (codeAttributes != null) {
      attributes.addAllWithKey(codeAttributes);
      codeAttributes = null;
    }
  }

  @Override
  protected StructGeneralAttribute readAttribute(DataInputFullStream in, ConstantPool pool, String name) throws IOException {
    if (StructGeneralAttribute.ATTRIBUTE_CODE.equals(name)) {
      if (!classStruct.isOwn()) {
        // skip code in foreign classes
        in.discard(8);
        in.discard(in.readInt());
        in.discard(8 * in.readUnsignedShort());
      }
      else {
        containsCode = true;
        in.discard(6);
        localVariables = in.readUnsignedShort();
        codeLength = in.readInt();
        in.discard(codeLength);
        int excLength = in.readUnsignedShort();
        in.discard(excLength * 8);
        codeFullLength = codeLength + excLength * 8 + 2;
      }

      codeAttributes = readAttributes(in, pool);

      return null;
    }

    return super.readAttribute(in, pool, name);
  }

  public void expandData() throws IOException {
    if (containsCode && !expanded) {
      byte[] code = classStruct.getLoader().loadBytecode(this, codeFullLength);
      seq = parseBytecode(new DataInputFullStream(code), codeLength, classStruct.getPool());
      expanded = true;
    }
  }

  public void releaseResources() throws IOException {
    if (containsCode && expanded) {
      seq = null;
      expanded = false;
    }
  }

  @SuppressWarnings("AssignmentToForLoopParameter")
  private InstructionSequence parseBytecode(DataInputFullStream in, int length, ConstantPool pool) throws IOException {
    VBStyleCollection<Instruction, Integer> instructions = new VBStyleCollection<Instruction, Integer>();

    int bytecode_version = classStruct.getBytecodeVersion();

    for (int i = 0; i < length; ) {

      int offset = i;

      int opcode = in.readUnsignedByte();
      int group = GROUP_GENERAL;

      boolean wide = (opcode == opc_wide);

      if (wide) {
        i++;
        opcode = in.readUnsignedByte();
      }

      List<Integer> operands = new ArrayList<Integer>();

      if (opcode >= opc_iconst_m1 && opcode <= opc_iconst_5) {
        operands.add(new Integer(opr_iconst[opcode - opc_iconst_m1]));
        opcode = opc_bipush;
      }
      else if (opcode >= opc_iload_0 && opcode <= opc_aload_3) {
        operands.add(new Integer(opr_loadstore[opcode - opc_iload_0]));
        opcode = opcs_load[(opcode - opc_iload_0) / 4];
      }
      else if (opcode >= opc_istore_0 && opcode <= opc_astore_3) {
        operands.add(new Integer(opr_loadstore[opcode - opc_istore_0]));
        opcode = opcs_store[(opcode - opc_istore_0) / 4];
      }
      else {
        switch (opcode) {
          case opc_bipush:
            operands.add(new Integer(in.readByte()));
            i++;
            break;
          case opc_ldc:
          case opc_newarray:
            operands.add(new Integer(in.readUnsignedByte()));
            i++;
            break;
          case opc_sipush:
          case opc_ifeq:
          case opc_ifne:
          case opc_iflt:
          case opc_ifge:
          case opc_ifgt:
          case opc_ifle:
          case opc_if_icmpeq:
          case opc_if_icmpne:
          case opc_if_icmplt:
          case opc_if_icmpge:
          case opc_if_icmpgt:
          case opc_if_icmple:
          case opc_if_acmpeq:
          case opc_if_acmpne:
          case opc_goto:
          case opc_jsr:
          case opc_ifnull:
          case opc_ifnonnull:
            if (opcode != opc_sipush) {
              group = GROUP_JUMP;
            }
            operands.add(new Integer(in.readShort()));
            i += 2;
            break;
          case opc_ldc_w:
          case opc_ldc2_w:
          case opc_getstatic:
          case opc_putstatic:
          case opc_getfield:
          case opc_putfield:
          case opc_invokevirtual:
          case opc_invokespecial:
          case opc_invokestatic:
          case opc_new:
          case opc_anewarray:
          case opc_checkcast:
          case opc_instanceof:
            operands.add(new Integer(in.readUnsignedShort()));
            i += 2;
            if (opcode >= opc_getstatic && opcode <= opc_putfield) {
              group = GROUP_FIELDACCESS;
            }
            else if (opcode >= opc_invokevirtual && opcode <= opc_invokestatic) {
              group = GROUP_INVOCATION;
            }
            break;
          case opc_invokedynamic:
            if (classStruct.isVersionGE_1_7()) { // instruction unused in Java 6 and before
              operands.add(new Integer(in.readUnsignedShort()));
              in.discard(2);
              group = GROUP_INVOCATION;
              i += 4;
            }
            break;
          case opc_iload:
          case opc_lload:
          case opc_fload:
          case opc_dload:
          case opc_aload:
          case opc_istore:
          case opc_lstore:
          case opc_fstore:
          case opc_dstore:
          case opc_astore:
          case opc_ret:
            if (wide) {
              operands.add(new Integer(in.readUnsignedShort()));
              i += 2;
            }
            else {
              operands.add(new Integer(in.readUnsignedByte()));
              i++;
            }
            if (opcode == opc_ret) {
              group = GROUP_RETURN;
            }
            break;
          case opc_iinc:
            if (wide) {
              operands.add(new Integer(in.readUnsignedShort()));
              operands.add(new Integer(in.readShort()));
              i += 4;
            }
            else {
              operands.add(new Integer(in.readUnsignedByte()));
              operands.add(new Integer(in.readByte()));
              i += 2;
            }
            break;
          case opc_goto_w:
          case opc_jsr_w:
            opcode = opcode == opc_jsr_w ? opc_jsr : opc_goto;
            operands.add(new Integer(in.readInt()));
            group = GROUP_JUMP;
            i += 4;
            break;
          case opc_invokeinterface:
            operands.add(new Integer(in.readUnsignedShort()));
            operands.add(new Integer(in.readUnsignedByte()));
            in.discard(1);
            group = GROUP_INVOCATION;
            i += 4;
            break;
          case opc_multianewarray:
            operands.add(new Integer(in.readUnsignedShort()));
            operands.add(new Integer(in.readUnsignedByte()));
            i += 3;
            break;
          case opc_tableswitch:
            in.discard((4 - (i + 1) % 4) % 4);
            i += ((4 - (i + 1) % 4) % 4); // padding
            operands.add(new Integer(in.readInt()));
            i += 4;
            int low = in.readInt();
            operands.add(new Integer(low));
            i += 4;
            int high = in.readInt();
            operands.add(new Integer(high));
            i += 4;

            for (int j = 0; j < high - low + 1; j++) {
              operands.add(new Integer(in.readInt()));
              i += 4;
            }
            group = GROUP_SWITCH;

            break;
          case opc_lookupswitch:
            in.discard((4 - (i + 1) % 4) % 4);
            i += ((4 - (i + 1) % 4) % 4); // padding
            operands.add(new Integer(in.readInt()));
            i += 4;
            int npairs = in.readInt();
            operands.add(new Integer(npairs));
            i += 4;

            for (int j = 0; j < npairs; j++) {
              operands.add(new Integer(in.readInt()));
              i += 4;
              operands.add(new Integer(in.readInt()));
              i += 4;
            }
            group = GROUP_SWITCH;
            break;
          case opc_ireturn:
          case opc_lreturn:
          case opc_freturn:
          case opc_dreturn:
          case opc_areturn:
          case opc_return:
          case opc_athrow:
            group = GROUP_RETURN;
        }
      }

      int[] ops = new int[operands.size()];
      for (int j = 0; j < operands.size(); j++) {
        ops[j] = operands.get(j).intValue();
      }

      Instruction instr = ConstantsUtil.getInstructionInstance(opcode, wide, group, bytecode_version, ops);

      instructions.addWithKey(instr, new Integer(offset));

      i++;
    }

    // initialize exception table
    List<ExceptionHandler> lstHandlers = new ArrayList<ExceptionHandler>();

    int exception_count = in.readUnsignedShort();
    for (int i = 0; i < exception_count; i++) {
      ExceptionHandler handler = new ExceptionHandler();
      handler.from = in.readUnsignedShort();
      handler.to = in.readUnsignedShort();
      handler.handler = in.readUnsignedShort();

      int excclass = in.readUnsignedShort();
      handler.class_index = excclass;
      if (excclass != 0) {
        handler.exceptionClass = pool.getPrimitiveConstant(excclass).getString();
      }

      lstHandlers.add(handler);
    }

    InstructionSequence seq = new FullInstructionSequence(instructions, new ExceptionTable(lstHandlers));

    // initialize instructions
    int i = seq.length() - 1;
    seq.setPointer(i);

    while (i >= 0) {
      Instruction instr = seq.getInstr(i--);
      if (instr.group != GROUP_GENERAL) {
        instr.initInstruction(seq);
      }
      seq.addToPointer(-1);
    }

    return seq;
  }

  public StructClass getClassStruct() {
    return classStruct;
  }

  public String getName() {
    return name;
  }

  public String getDescriptor() {
    return descriptor;
  }

  public boolean containsCode() {
    return containsCode;
  }

  public int getLocalVariables() {
    return localVariables;
  }

  public InstructionSequence getInstructionSequence() {
    return seq;
  }
}
