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

import dexfuzz.Log;
import dexfuzz.MutationStats;
import dexfuzz.program.MBranchInsn;
import dexfuzz.program.MInsn;
import dexfuzz.program.MutatableCode;
import dexfuzz.program.Mutation;
import dexfuzz.rawdex.Instruction;
import dexfuzz.rawdex.Opcode;
import dexfuzz.rawdex.OpcodeInfo;
import dexfuzz.rawdex.formats.AbstractFormat;
import dexfuzz.rawdex.formats.ContainsConst;
import dexfuzz.rawdex.formats.ContainsPoolIndex;
import dexfuzz.rawdex.formats.ContainsPoolIndex.PoolIndexKind;
import dexfuzz.rawdex.formats.ContainsVRegs;

import java.util.List;
import java.util.Random;

public class RandomInstructionGenerator extends CodeMutator {
  /**
   * Every CodeMutator has an AssociatedMutation, representing the
   * mutation that this CodeMutator can perform, to allow separate
   * generateMutation() and applyMutation() phases, allowing serialization.
   */
  public static class AssociatedMutation extends Mutation {
    public int insertionIdx;
    public int newOpcode;
    public boolean hasConst;
    public long constValue;
    public boolean hasPoolIndex;
    public int poolIndexValue;
    public boolean hasVregs;
    public int vregCount;
    public int vregA;
    public int vregB;
    public int vregC;
    public int branchTargetIdx;

    @Override
    public String getString() {
      String result = String.format("%d %d %s %d %s %d %s %d %d %d %d %d",
          insertionIdx,
          newOpcode,
          (hasConst ? "T" : "F"),
          constValue,
          (hasPoolIndex ? "T" : "F"),
          poolIndexValue,
          (hasVregs ? "T" : "F"),
          vregCount,
          vregA,
          vregB,
          vregC,
          branchTargetIdx
          );
      return result;
    }

    @Override
    public void parseString(String[] elements) {
      insertionIdx = Integer.parseInt(elements[2]);
      newOpcode = Integer.parseInt(elements[3]);
      hasConst = (elements[4].equals("T"));
      constValue = Long.parseLong(elements[5]);
      hasPoolIndex = (elements[6].equals("T"));
      poolIndexValue = Integer.parseInt(elements[7]);
      hasVregs = (elements[8].equals("T"));
      vregCount = Integer.parseInt(elements[9]);
      vregA = Integer.parseInt(elements[10]);
      vregB = Integer.parseInt(elements[11]);
      vregC = Integer.parseInt(elements[12]);
      branchTargetIdx = Integer.parseInt(elements[13]);
    }
  }

  // The following two methods are here for the benefit of MutationSerializer,
  // so it can create a CodeMutator and get the correct associated Mutation, as it
  // reads in mutations from a dump of mutations.
  @Override
  public Mutation getNewMutation() {
    return new AssociatedMutation();
  }

  public RandomInstructionGenerator() { }

  public RandomInstructionGenerator(Random rng, MutationStats stats, List<Mutation> mutations) {
    super(rng, stats, mutations);
    likelihood = 30;
  }

  @Override
  protected Mutation generateMutation(MutatableCode mutatableCode) {
    // Find the insertion point.
    int insertionIdx = 0;
    boolean foundInsn = false;

    while (!foundInsn) {
      insertionIdx = rng.nextInt(mutatableCode.getInstructionCount());
      MInsn insertionPoint =
          mutatableCode.getInstructionAt(insertionIdx);
      foundInsn = true;

      // Don't want to insert instructions where there are raw instructions for now.
      if (insertionPoint.insn.justRaw) {
        foundInsn = false;
      }
    }

    Opcode newOpcode = null;
    int opcodeCount = Opcode.values().length;
    boolean foundOpcode = false;

    while (!foundOpcode) {
      newOpcode = Opcode.values()[rng.nextInt(opcodeCount)];
      foundOpcode = true;
      if (Opcode.isBetween(newOpcode, Opcode.FILLED_NEW_ARRAY, Opcode.FILL_ARRAY_DATA)
          || Opcode.isBetween(newOpcode, Opcode.PACKED_SWITCH, Opcode.SPARSE_SWITCH)
          || Opcode.isBetween(newOpcode, Opcode.INVOKE_VIRTUAL, Opcode.INVOKE_INTERFACE)
          || Opcode.isBetween(newOpcode, Opcode.INVOKE_VIRTUAL_RANGE, Opcode.INVOKE_INTERFACE_RANGE)
          || Opcode.isBetween(newOpcode, Opcode.UNUSED_3E, Opcode.UNUSED_43)
          || Opcode.isBetween(newOpcode, Opcode.UNUSED_79, Opcode.UNUSED_7A)
          || Opcode.isBetween(newOpcode, Opcode.UNUSED_E3, Opcode.UNUSED_FF)) {
        foundOpcode = false;
      }
    }

    OpcodeInfo newOpcodeInfo = Instruction.getOpcodeInfo(newOpcode);

    AssociatedMutation mutation = new AssociatedMutation();
    mutation.setup(this.getClass(), mutatableCode);
    mutation.insertionIdx = insertionIdx;
    mutation.newOpcode = newOpcodeInfo.value;

    AbstractFormat fmt = newOpcodeInfo.format;

    if (fmt instanceof ContainsConst) {
      mutation.hasConst = true;
      mutation.constValue = rng.nextLong() % ((ContainsConst)fmt).getConstRange();
    }
    if (fmt instanceof ContainsPoolIndex) {
      mutation.hasPoolIndex = true;
      ContainsPoolIndex containsPoolIndex = (ContainsPoolIndex) fmt;
      PoolIndexKind poolIndexKind = containsPoolIndex.getPoolIndexKind(newOpcodeInfo);
      int maxPoolIndex = mutatableCode.program.getTotalPoolIndicesByKind(poolIndexKind);
      if (maxPoolIndex > 0) {
        mutation.poolIndexValue = rng.nextInt(maxPoolIndex);
      } else {
        mutation.poolIndexValue = 0;
      }
    }
    if (mutatableCode.registersSize == 0) {
      mutatableCode.registersSize = 1;
    }
    if (fmt instanceof ContainsVRegs) {
      mutation.hasVregs = true;
      ContainsVRegs containsVregs = (ContainsVRegs) fmt;
      mutation.vregCount = containsVregs.getVRegCount();
      switch (mutation.vregCount) {
        case 3:
          mutation.vregC = rng.nextInt(mutatableCode.registersSize);
          // fallthrough
        case 2:
          mutation.vregB = rng.nextInt(mutatableCode.registersSize);
          // fallthrough
        case 1:
          mutation.vregA = rng.nextInt(mutatableCode.registersSize);
          break;
        default:
          Log.errorAndQuit("Invalid number of vregs specified.");
      }
    }
    // If we have some kind of branch, pick a random target.
    if (Opcode.isBetween(newOpcode, Opcode.IF_EQ, Opcode.IF_LEZ)
        || Opcode.isBetween(newOpcode, Opcode.GOTO, Opcode.GOTO_32)) {
      mutation.branchTargetIdx = rng.nextInt(mutatableCode.getInstructionCount());
    }

    return mutation;
  }

  @Override
  protected void applyMutation(Mutation uncastMutation) {
    // Cast the Mutation to our AssociatedMutation, so we can access its fields.
    AssociatedMutation mutation = (AssociatedMutation) uncastMutation;
    MutatableCode mutatableCode = mutation.mutatableCode;

    Opcode newOpcode = Instruction.getOpcodeInfo(mutation.newOpcode).opcode;

    boolean isBranch = false;
    if (Opcode.isBetween(newOpcode, Opcode.IF_EQ, Opcode.IF_LEZ)
        || Opcode.isBetween(newOpcode, Opcode.GOTO, Opcode.GOTO_32)) {
      isBranch = true;
    }

    MInsn newInsn = null;
    if (!isBranch) {
      newInsn = new MInsn();
    } else {
      newInsn = new MBranchInsn();
    }
    newInsn.insn = new Instruction();
    newInsn.insn.info = Instruction.getOpcodeInfo(mutation.newOpcode);
    AbstractFormat fmt = newInsn.insn.info.format;

    if (mutation.hasConst) {
      ContainsConst containsConst = (ContainsConst) fmt;
      containsConst.setConst(newInsn.insn, mutation.constValue);
    }
    if (mutation.hasPoolIndex) {
      ContainsPoolIndex containsPoolIndex = (ContainsPoolIndex) fmt;
      containsPoolIndex.setPoolIndex(newInsn.insn, mutation.poolIndexValue);
    }
    if (mutation.hasVregs) {
      switch (mutation.vregCount) {
        case 3:
          newInsn.insn.vregC = mutation.vregC;
          // fallthrough
        case 2:
          newInsn.insn.vregB = mutation.vregB;
          // fallthrough
        case 1:
          newInsn.insn.vregA = mutation.vregA;
          break;
        default:
          Log.errorAndQuit("Invalid number of vregs specified.");
      }
    }

    if (isBranch) {
      // We have a branch instruction, point it at its target.
      MBranchInsn newBranchInsn = (MBranchInsn) newInsn;
      newBranchInsn.target = mutatableCode.getInstructionAt(mutation.branchTargetIdx);
    }

    MInsn insertionPoint =
        mutatableCode.getInstructionAt(mutation.insertionIdx);

    Log.info("Generated random instruction: " + newInsn
        + ", inserting at " + insertionPoint);

    stats.incrementStat("Generated random instruction");

    mutatableCode.insertInstructionAt(newInsn, mutation.insertionIdx);

    // If we've generated a monitor insn, generate the matching opposing insn.
    if (newInsn.insn.info.opcode == Opcode.MONITOR_ENTER) {
      MInsn exitInsn = newInsn.clone();
      exitInsn.insn.info = Instruction.getOpcodeInfo(Opcode.MONITOR_EXIT);
      mutatableCode.insertInstructionAfter(exitInsn, mutation.insertionIdx);
      Log.info("Generated matching monitor-exit: " + exitInsn);
    } else if (newInsn.insn.info.opcode == Opcode.MONITOR_EXIT) {
      MInsn enterInsn = newInsn.clone();
      enterInsn.insn.info = Instruction.getOpcodeInfo(Opcode.MONITOR_ENTER);
      mutatableCode.insertInstructionAt(enterInsn, mutation.insertionIdx);
      Log.info("Generated matching monitor-enter: " + enterInsn);
    }
  }
}
