/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * 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 dexfuzz.program;

import dexfuzz.Log;
import dexfuzz.rawdex.CodeItem;
import dexfuzz.rawdex.EncodedCatchHandler;
import dexfuzz.rawdex.EncodedTypeAddrPair;
import dexfuzz.rawdex.Instruction;
import dexfuzz.rawdex.Opcode;
import dexfuzz.rawdex.TryItem;
import dexfuzz.rawdex.formats.ContainsTarget;
import dexfuzz.rawdex.formats.RawInsnHelper;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Translates from a CodeItem (the raw list of Instructions) to MutatableCode
 * (graph of Instructions, using MInsns and subclasses) and vice-versa.
 */
public class CodeTranslator {

  /**
   * Given a raw DEX file's CodeItem, produce a MutatableCode object, that CodeMutators
   * are designed to operate on.
   * @param codeItemIdx Used to make sure the correct CodeItem is updated later after mutation.
   * @return A new MutatableCode object, which contains all relevant information
   *         obtained from the CodeItem.
   */
  public MutatableCode codeItemToMutatableCode(Program program, CodeItem codeItem,
      int codeItemIdx, int mutatableCodeIdx) {
    Log.debug("Translating CodeItem " + codeItemIdx
        + " (" + codeItem.meta.methodName + ") to MutatableCode");

    MutatableCode mutatableCode = new MutatableCode(program);

    codeItem.registerMutatableCode(mutatableCode);

    mutatableCode.name = codeItem.meta.methodName;
    mutatableCode.shorty = codeItem.meta.shorty;
    mutatableCode.isStatic = codeItem.meta.isStatic;

    mutatableCode.codeItemIdx = codeItemIdx;

    mutatableCode.mutatableCodeIdx = mutatableCodeIdx;

    mutatableCode.registersSize = codeItem.registersSize;
    mutatableCode.insSize = codeItem.insSize;
    mutatableCode.outsSize = codeItem.outsSize;
    mutatableCode.triesSize = codeItem.triesSize;

    // Temporary map from bytecode offset -> instruction.
    Map<Integer,MInsn> insnLocationMap = new HashMap<Integer,MInsn>();

    List<Instruction> inputInsns = codeItem.insns;

    // Create the MInsns.
    int loc = 0;
    for (Instruction insn : inputInsns) {
      MInsn mInsn = null;

      if (isInstructionSwitch(insn)) {
        mInsn = new MSwitchInsn();
      } else if (isInstructionBranch(insn)) {
        mInsn = new MBranchInsn();
      } else if (isInstructionFillArrayData(insn)) {
        mInsn = new MInsnWithData();
      } else {
        mInsn = new MInsn();
      }

      mInsn.insn = insn;

      // Populate the temporary map.
      insnLocationMap.put(loc, mInsn);

      // Populate the proper list of mutatable instructions.
      mutatableCode.addInstructionToEnd(mInsn);

      // Calculate the offsets for each instruction.
      mInsn.location = loc;
      mInsn.locationUpdated = false;

      loc += mInsn.insn.getSize();
    }

    // Now make branch/switch instructions point at the right target instructions.
    for (MInsn mInsn : mutatableCode.getInstructions()) {
      if (mInsn instanceof MSwitchInsn) {
        readSwitchInstruction((MSwitchInsn) mInsn, insnLocationMap);
      } else if (mInsn instanceof MInsnWithData) {
        ContainsTarget containsTarget = (ContainsTarget) mInsn.insn.info.format;
        int targetLoc = mInsn.location + (int) containsTarget.getTarget(mInsn.insn);
        ((MInsnWithData)mInsn).dataTarget = insnLocationMap.get(targetLoc);
        if (((MInsnWithData)mInsn).dataTarget == null) {
          Log.errorAndQuit("Bad offset calculation in data-target insn");
        }
      } else if (mInsn instanceof MBranchInsn) {
        ContainsTarget containsTarget = (ContainsTarget) mInsn.insn.info.format;
        int targetLoc = mInsn.location + (int) containsTarget.getTarget(mInsn.insn);
        ((MBranchInsn)mInsn).target = insnLocationMap.get(targetLoc);
        if (((MBranchInsn)mInsn).target == null) {
          Log.errorAndQuit("Bad offset calculation in branch insn");
        }
      }
    }

    // Now create try blocks.
    if (mutatableCode.triesSize > 0) {
      readTryBlocks(codeItem, mutatableCode, insnLocationMap);
    }

    return mutatableCode;
  }

  /**
   * Given a MutatableCode item that may have been mutated, update the original CodeItem
   * correctly, to allow valid DEX to be written back to the output file.
   */
  public void mutatableCodeToCodeItem(CodeItem codeItem, MutatableCode mutatableCode) {
    Log.debug("Translating MutatableCode " + mutatableCode.name
        + " to CodeItem " + mutatableCode.codeItemIdx);

    // We must first align any data instructions at the end of the code
    // before we recalculate any offsets.
    // This also updates their sizes...
    alignDataInstructions(mutatableCode);

    // Validate that the tracked locations for instructions are valid.
    // Also mark locations as no longer being updated.
    int loc = 0;
    for (MInsn mInsn : mutatableCode.getInstructions()) {
      if (mInsn.insn.justRaw) {
        // All just_raw instructions need alignment!
        if ((loc % 2) != 0) {
          loc++;
        }
      }
      if (mInsn.location != loc) {
        Log.errorAndQuit(String.format("%s does not have expected location 0x%x",
            mInsn, loc));
      }
      mInsn.locationUpdated = false;
      loc += mInsn.insn.getSize();
    }

    // This new list will be attached to the CodeItem at the end...
    List<Instruction> outputInsns = new LinkedList<Instruction>();

    // Go through our new list of MInsns, adding them to the new
    // list of instructions that will be attached to the CodeItem.
    // Also recalculate offsets for branches.
    for (MInsn mInsn : mutatableCode.getInstructions()) {
      if (mInsn instanceof MSwitchInsn) {
        updateSwitchInstruction((MSwitchInsn)mInsn, mutatableCode);
      } else if (mInsn instanceof MInsnWithData) {
        MInsn target = ((MInsnWithData) mInsn).dataTarget;
        int dataOffset = target.location - mInsn.location;
        ContainsTarget containsTarget = (ContainsTarget) mInsn.insn.info.format;
        containsTarget.setTarget(mInsn.insn, dataOffset);
      } else if (mInsn instanceof MBranchInsn) {
        MInsn target = ((MBranchInsn) mInsn).target;
        int branchOffset = target.location - mInsn.location;
        ContainsTarget containsTarget = (ContainsTarget) mInsn.insn.info.format;
        containsTarget.setTarget(mInsn.insn, branchOffset);
      }
      outputInsns.add(mInsn.insn);
    }

    // Calculate the new insns_size.
    int newInsnsSize = 0;
    for (Instruction insn : outputInsns) {
      newInsnsSize += insn.getSize();
    }

    if (mutatableCode.triesSize > 0) {
      updateTryBlocks(codeItem, mutatableCode);
    }

    codeItem.insnsSize = newInsnsSize;
    codeItem.insns = outputInsns;
    codeItem.registersSize = mutatableCode.registersSize;
    codeItem.insSize = mutatableCode.insSize;
    codeItem.outsSize = mutatableCode.outsSize;
    codeItem.triesSize = mutatableCode.triesSize;
  }

  /**
   * The TryItem specifies an offset into the EncodedCatchHandlerList for a given CodeItem,
   * but we only have an array of the EncodedCatchHandlers that the List contains.
   * This function produces a map that offers a way to find out the index into our array,
   * from the try handler's offset.
   */
  private Map<Short,Integer> createTryHandlerOffsetToIndexMap(CodeItem codeItem) {
    // Create a sorted set of offsets.
    List<Short> uniqueOffsets = new ArrayList<Short>();
    for (TryItem tryItem : codeItem.tries) {
      int index = 0;
      while (true) {
        if ((index == uniqueOffsets.size())
            || (uniqueOffsets.get(index) > tryItem.handlerOff)) {
          // First condition means we're at the end of the set (or we're inserting
          //   into an empty set)
          // Second condition means that the offset belongs here
          // ...so insert it here, pushing the element currently in this position to the
          //    right, if it exists
          uniqueOffsets.add(index, tryItem.handlerOff);
          break;
        } else if (uniqueOffsets.get(index) == tryItem.handlerOff) {
          // We've already seen it, and we're making a set, not a list.
          break;
        } else {
          // Keep searching.
          index++;
        }
      }
    }
    // Now we have an (implicit) index -> offset mapping!

    // Now create the reverse mapping.
    Map<Short,Integer> offsetIndexMap = new HashMap<Short,Integer>();
    for (int i = 0; i < uniqueOffsets.size(); i++) {
      offsetIndexMap.put(uniqueOffsets.get(i), i);
    }

    return offsetIndexMap;
  }

  private void readTryBlocks(CodeItem codeItem, MutatableCode mutatableCode,
      Map<Integer,MInsn> insnLocationMap) {
    mutatableCode.mutatableTries = new LinkedList<MTryBlock>();

    Map<Short,Integer> offsetIndexMap = createTryHandlerOffsetToIndexMap(codeItem);

    // Read each TryItem into a MutatableTryBlock.
    for (TryItem tryItem : codeItem.tries) {
      MTryBlock mTryBlock = new MTryBlock();

      // Get the MInsns that form the start and end of the try block.
      int startLocation = tryItem.startAddr;
      mTryBlock.startInsn = insnLocationMap.get(startLocation);
      int endLocation = tryItem.startAddr + tryItem.insnCount;
      mTryBlock.endInsn = insnLocationMap.get(endLocation);

      // Sanity checks.
      if (mTryBlock.startInsn == null) {
        Log.errorAndQuit(String.format(
            "Couldn't find a mutatable insn at start offset 0x%x",
            startLocation));
      }
      if (mTryBlock.endInsn == null) {
        Log.errorAndQuit(String.format(
            "Couldn't find a mutatable insn at end offset 0x%x",
            endLocation));
      }

      // Get the EncodedCatchHandler.
      int handlerIdx = offsetIndexMap.get(tryItem.handlerOff);
      EncodedCatchHandler encodedCatchHandler = codeItem.handlers.list[handlerIdx];

      // Do we have a catch all? If so, associate the MInsn that's there.
      if (encodedCatchHandler.size <= 0) {
        mTryBlock.catchAllHandler =
            insnLocationMap.get(encodedCatchHandler.catchAllAddr);
        // Sanity check.
        if (mTryBlock.catchAllHandler == null) {
          Log.errorAndQuit(
              String.format("Couldn't find a mutatable insn at catch-all offset 0x%x",
                  encodedCatchHandler.catchAllAddr));
        }
      }
      // Do we have explicitly-typed handlers? This will remain empty if not.
      mTryBlock.handlers = new LinkedList<MInsn>();

      // Associate all the explicitly-typed handlers.
      for (int i = 0; i < Math.abs(encodedCatchHandler.size); i++) {
        EncodedTypeAddrPair handler = encodedCatchHandler.handlers[i];
        MInsn handlerInsn = insnLocationMap.get(handler.addr);
        // Sanity check.
        if (handlerInsn == null) {
          Log.errorAndQuit(String.format(
              "Couldn't find a mutatable instruction at handler offset 0x%x",
              handler.addr));
        }
        mTryBlock.handlers.add(handlerInsn);
      }

      // Now finally add the new MutatableTryBlock into this MutatableCode's list!
      mutatableCode.mutatableTries.add(mTryBlock);
    }
  }

  private void updateTryBlocks(CodeItem codeItem, MutatableCode mutatableCode) {

    // TODO: Support ability to add extra try blocks/handlers?

    for (MTryBlock mTryBlock : mutatableCode.mutatableTries) {
      if (mTryBlock.startInsn.location > mTryBlock.endInsn.location) {
        // Mutation has put this try block's end insn before its start insn. Fix this.
        MInsn tempInsn = mTryBlock.startInsn;
        mTryBlock.startInsn = mTryBlock.endInsn;
        mTryBlock.endInsn = tempInsn;
      }
    }

    // First, manipulate the try blocks if they overlap.
    for (int i = 0; i < mutatableCode.mutatableTries.size() - 1; i++) {
      MTryBlock first = mutatableCode.mutatableTries.get(i);
      MTryBlock second = mutatableCode.mutatableTries.get(i + 1);

      // Do they overlap?
      if (first.endInsn.location > second.startInsn.location) {

        Log.debug("Found overlap in TryBlocks, moving 2nd TryBlock...");
        Log.debug("1st TryBlock goes from " + first.startInsn + " to "  + first.endInsn);
        Log.debug("2nd TryBlock goes from " + second.startInsn + " to "  + second.endInsn);

        // Find the first instruction that comes after that does not overlap
        // with the first try block.
        MInsn newInsn = second.startInsn;
        int ptr = mutatableCode.getInstructionIndex(newInsn);
        while (first.endInsn.location > newInsn.location) {
          ptr++;
          newInsn = mutatableCode.getInstructionAt(ptr);
        }
        second.startInsn = newInsn;

        Log.debug("Now 2nd TryBlock goes from " + second.startInsn + " to "  + second.endInsn);
      }
    }

    Map<Short,Integer> offsetIndexMap = createTryHandlerOffsetToIndexMap(codeItem);

    int tryItemIdx = 0;
    for (MTryBlock mTryBlock : mutatableCode.mutatableTries) {
      TryItem tryItem = codeItem.tries[tryItemIdx];

      tryItem.startAddr = mTryBlock.startInsn.location;
      tryItem.insnCount =
          (short) (mTryBlock.endInsn.location - mTryBlock.startInsn.location);

      // Get the EncodedCatchHandler.
      EncodedCatchHandler encodedCatchHandler =
          codeItem.handlers.list[offsetIndexMap.get(tryItem.handlerOff)];

      if (encodedCatchHandler.size <= 0) {
        encodedCatchHandler.catchAllAddr = mTryBlock.catchAllHandler.location;
      }
      for (int i = 0; i < Math.abs(encodedCatchHandler.size); i++) {
        MInsn handlerInsn = mTryBlock.handlers.get(i);
        EncodedTypeAddrPair handler = encodedCatchHandler.handlers[i];
        handler.addr = handlerInsn.location;
      }
      tryItemIdx++;
    }
  }

  /**
   * Given a switch instruction, find the associated data's raw[] form, and update
   * the targets of the switch instruction to point to the correct instructions.
   */
  private void readSwitchInstruction(MSwitchInsn switchInsn,
      Map<Integer,MInsn> insnLocationMap) {
    // Find the data.
    ContainsTarget containsTarget = (ContainsTarget) switchInsn.insn.info.format;
    int dataLocation = switchInsn.location + (int) containsTarget.getTarget(switchInsn.insn);
    switchInsn.dataTarget = insnLocationMap.get(dataLocation);
    if (switchInsn.dataTarget == null) {
      Log.errorAndQuit("Bad offset calculation for data target in switch insn");
    }

    // Now read the data.
    Instruction dataInsn = switchInsn.dataTarget.insn;

    int rawPtr = 2;

    int targetsSize = (int) RawInsnHelper.getUnsignedShortFromTwoBytes(dataInsn.rawBytes, rawPtr);
    rawPtr += 2;

    int[] keys = new int[targetsSize];
    int[] targets = new int[targetsSize];

    if (dataInsn.rawType == 1) {
      switchInsn.packed = true;
      // Dealing with a packed-switch.
      // Read the first key.
      keys[0] = (int) RawInsnHelper.getUnsignedIntFromFourBytes(dataInsn.rawBytes, rawPtr);
      rawPtr += 4;
      // Calculate the rest of the keys.
      for (int i = 1; i < targetsSize; i++) {
        keys[i] = keys[i - 1] + 1;
      }
    } else if (dataInsn.rawType == 2) {
      // Dealing with a sparse-switch.
      // Read all of the keys.
      for (int i = 0; i < targetsSize; i++) {
        keys[i] = (int) RawInsnHelper.getUnsignedIntFromFourBytes(dataInsn.rawBytes,
            rawPtr);
        rawPtr += 4;
      }
    }

    // Now read the targets.
    for (int i = 0; i < targetsSize; i++) {
      targets[i] = (int) RawInsnHelper.getUnsignedIntFromFourBytes(dataInsn.rawBytes,
          rawPtr);
      rawPtr += 4;
    }

    // Store the keys.
    switchInsn.keys = keys;

    // Convert our targets[] offsets into pointers to MInsns.
    for (int target : targets) {
      int targetLocation = switchInsn.location + target;
      MInsn targetInsn = insnLocationMap.get(targetLocation);
      switchInsn.targets.add(targetInsn);
      if (targetInsn == null) {
        Log.errorAndQuit("Bad offset calculation for target in switch insn");
      }
    }
  }

  /**
   * Given a mutatable switch instruction, which may have had some of its branch
   * targets moved, update all the target offsets in the raw[] form of the instruction.
   */
  private void updateSwitchInstruction(MSwitchInsn switchInsn, MutatableCode mutatableCode) {
    // Update the offset to the data instruction
    MInsn dataTarget = switchInsn.dataTarget;
    int dataOffset = dataTarget.location - switchInsn.location;
    ContainsTarget containsTarget = (ContainsTarget) switchInsn.insn.info.format;
    containsTarget.setTarget(switchInsn.insn, dataOffset);

    int targetsSize = switchInsn.targets.size();

    int[] keys = switchInsn.keys;
    int[] targets = new int[targetsSize];

    // Calculate the new offsets.
    int targetIdx = 0;
    for (MInsn target : switchInsn.targets) {
      targets[targetIdx] = target.location - switchInsn.location;
      targetIdx++;
    }

    // Now write the data back to the raw bytes.
    Instruction dataInsn = switchInsn.dataTarget.insn;

    int rawPtr = 2;

    // Write out the size.
    RawInsnHelper.writeUnsignedShortToTwoBytes(dataInsn.rawBytes, rawPtr, targetsSize);
    rawPtr += 2;

    // Write out the keys.
    if (switchInsn.packed) {
      // Only write out one key - the first.
      RawInsnHelper.writeUnsignedIntToFourBytes(dataInsn.rawBytes, rawPtr, keys[0]);
      rawPtr += 4;
    } else {
      // Write out all the keys.
      for (int i = 0; i < targetsSize; i++) {
        RawInsnHelper.writeUnsignedIntToFourBytes(dataInsn.rawBytes, rawPtr, keys[i]);
        rawPtr += 4;
      }
    }

    // Write out all the targets.
    for (int i = 0; i < targetsSize; i++) {
      RawInsnHelper.writeUnsignedIntToFourBytes(dataInsn.rawBytes, rawPtr, targets[i]);
      rawPtr += 4;
    }
  }

  /**
   * After mutation, data instructions may no longer be 4-byte aligned.
   * If this is the case, insert nops to align them all.
   * This makes a number of assumptions about data currently:
   * - data is always at the end of method insns
   * - all data instructions are stored contiguously
   */
  private void alignDataInstructions(MutatableCode mutatableCode) {
    // Find all the switch data instructions.
    List<MInsn> dataInsns = new ArrayList<MInsn>();

    // Update raw sizes of the data instructions as well.
    for (MInsn mInsn : mutatableCode.getInstructions()) {
      if (mInsn instanceof MSwitchInsn) {
        // Update the raw size of the instruction.
        MSwitchInsn switchInsn = (MSwitchInsn) mInsn;
        int targetsSize = switchInsn.targets.size();
        Instruction dataInsn = switchInsn.dataTarget.insn;
        if (switchInsn.packed) {
          dataInsn.rawSize = (targetsSize * 2) + 4;
        } else {
          dataInsn.rawSize = (targetsSize * 4) + 2;
        }
        dataInsns.add(switchInsn.dataTarget);
      } else if (mInsn instanceof MInsnWithData) {
        MInsnWithData insnWithData =
            (MInsnWithData) mInsn;
        dataInsns.add(insnWithData.dataTarget);
      }
    }

    // Only need to align switch data instructions if there are any!
    if (!dataInsns.isEmpty()) {

      Log.debug("Found data instructions, checking alignment...");

      // Sort data_insns by location.
      Collections.sort(dataInsns, new Comparator<MInsn>() {
        @Override
        public int compare(MInsn first, MInsn second) {
          if (first.location < second.location) {
            return -1;
          } else if (first.location > second.location) {
            return 1;
          }
          return 0;
        }
      });

      boolean performedAlignment = false;

      // Go through all the data insns, and insert an alignment nop if they're unaligned.
      for (MInsn dataInsn : dataInsns) {
        if (dataInsn.location % 2 != 0) {
          Log.debug("Aligning data instruction with a nop.");
          int alignmentNopIdx = mutatableCode.getInstructionIndex(dataInsn);
          MInsn nop = new MInsn();
          nop.insn = new Instruction();
          nop.insn.info = Instruction.getOpcodeInfo(Opcode.NOP);
          mutatableCode.insertInstructionAt(nop, alignmentNopIdx);
          performedAlignment = true;
        }
      }

      if (!performedAlignment) {
        Log.debug("Alignment okay.");
      }
    }
  }

  /**
   * Determine if a particular instruction is a branch instruction, based on opcode.
   */
  private boolean isInstructionBranch(Instruction insn) {
    Opcode opcode = insn.info.opcode;
    if (Opcode.isBetween(opcode, Opcode.IF_EQ, Opcode.IF_LEZ)
        || Opcode.isBetween(opcode, Opcode.GOTO, Opcode.GOTO_32)) {
      return true;
    }
    return false;
  }

  /**
   * Determine if a particular instruction is a switch instruction, based on opcode.
   */
  private boolean isInstructionSwitch(Instruction insn) {
    Opcode opcode = insn.info.opcode;
    if (Opcode.isBetween(opcode, Opcode.PACKED_SWITCH, Opcode.SPARSE_SWITCH)) {
      return true;
    }
    return false;
  }

  private boolean isInstructionFillArrayData(Instruction insn) {
    return (insn.info.opcode == Opcode.FILL_ARRAY_DATA);
  }
}
