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

import dexfuzz.Log;
import dexfuzz.Options;
import dexfuzz.Timer;
import dexfuzz.executors.Architecture;
import dexfuzz.executors.Arm64InterpreterExecutor;
import dexfuzz.executors.Arm64OptimizingBackendExecutor;
import dexfuzz.executors.ArmInterpreterExecutor;
import dexfuzz.executors.ArmOptimizingBackendExecutor;
import dexfuzz.executors.Device;
import dexfuzz.executors.Executor;
import dexfuzz.executors.X86InterpreterExecutor;
import dexfuzz.executors.X86OptimizingBackendExecutor;
import dexfuzz.executors.X86_64InterpreterExecutor;
import dexfuzz.executors.X86_64OptimizingBackendExecutor;
import dexfuzz.listeners.BaseListener;
import dexfuzz.program.Mutation;
import dexfuzz.program.Program;
import dexfuzz.rawdex.DexRandomAccessFile;
import dexfuzz.rawdex.OffsetTracker;
import dexfuzz.rawdex.RawDexFile;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A particular fuzzing strategy, this class provides the common methods
 * most fuzzing will involve, and subclasses override the run() method, to
 * employ a particular strategy.
 */
public abstract class Fuzzer {
  private List<Executor> executors;
  private OffsetTracker offsetTracker;

  /**
   * This is the executor that we use to test for self-divergent programs.
   */
  private Executor goldenExecutor;

  /*
   * These two flags are set during fuzz(), and then cleared at the end of execute().
   */
  private boolean mutatedSuccessfully;
  private boolean savedSuccessfully;

  private Timer totalTimer = new Timer("Total Time");
  private Timer timerDexInput = new Timer("DEX Input");
  private Timer timerProgGen = new Timer("Program Generation");
  private Timer timerMutation = new Timer("Mutation Time");
  private Timer timerDexOutput = new Timer("DEX Output");
  private Timer timerChecksumCalc = new Timer("Checksum Calculation");

  protected BaseListener listener;

  protected Fuzzer(BaseListener listener) {
    totalTimer.start();
    executors = new ArrayList<Executor>();
    this.listener = listener;
  }

  public abstract void run();

  protected abstract String getNextInputFilename();

  protected abstract String getNextOutputFilename();

  /**
   * Call this after fuzzer execution to print out timing results.
   */
  public void printTimingInfo() {
    totalTimer.stop();
    timerDexInput.printTime(listener);
    timerProgGen.printTime(listener);
    timerMutation.printTime(listener);
    timerDexOutput.printTime(listener);
    timerChecksumCalc.printTime(listener);
    totalTimer.printTime(listener);
  }

  /**
   * Make sure this is called to correctly shutdown each Executor's StreamConsumers.
   */
  public void shutdown() {
    if (executors != null) {
      for (Executor executor : executors) {
        executor.shutdown();
      }
    }
  }

  private void addExecutorsForArchitecture(Device device, Class<? extends Executor> optimizing,
      Class<? extends Executor> interpreter) {
    // NB: Currently OptimizingBackend MUST come immediately before same arch's Interpreter.
    // This is because intepreter execution relies on there being an OAT file already
    // created to produce correct debug information. Otherwise we will see
    // false-positive divergences.
    try {
      if (Options.useOptimizing) {
        Constructor<? extends Executor> constructor =
            optimizing.getConstructor(BaseListener.class, Device.class);
        executors.add(constructor.newInstance(listener, device));
      }
      if (Options.useInterpreter) {
        Constructor<? extends Executor> constructor =
            interpreter.getConstructor(BaseListener.class, Device.class);
        executors.add(constructor.newInstance(listener, device));
      }
    } catch (NoSuchMethodException e) {
      Log.errorAndQuit("Executor doesn't have correct constructor.");
    } catch (InstantiationException e) {
      Log.errorAndQuit("Executor couldn't be instantiated.");
    } catch (IllegalAccessException e) {
      Log.errorAndQuit("Executor couldn't be accessed.");
    } catch (IllegalArgumentException e) {
      Log.errorAndQuit("Invalid arguments to instantiation of Executor.");
    } catch (InvocationTargetException e) {
      Log.errorAndQuit("Instantiation of Executor threw an Exception!");
    }
  }

  protected void addExecutors() {
    Device device = null;
    if (Options.executeOnHost) {
      device = new Device();
    } else {
      device = new Device(Options.deviceName, Options.noBootImage);
    }

    if (Options.useArchArm64) {
      addExecutorsForArchitecture(device, Arm64OptimizingBackendExecutor.class,
          Arm64InterpreterExecutor.class);
    }

    if (Options.useArchArm) {
      addExecutorsForArchitecture(device, ArmOptimizingBackendExecutor.class,
          ArmInterpreterExecutor.class);
    }

    if (Options.useArchX86_64) {
      addExecutorsForArchitecture(device, X86_64OptimizingBackendExecutor.class,
          X86_64InterpreterExecutor.class);
    }

    if (Options.useArchX86) {
      addExecutorsForArchitecture(device, X86OptimizingBackendExecutor.class,
          X86InterpreterExecutor.class);
    }

    // Add the first backend as the golden executor for self-divergence tests.
    goldenExecutor = executors.get(0);
  }

  /**
   * Called from each Fuzzer subclass that we can instantiate. Parses the program, fuzzes it,
   * and then saves it, if mutation was successful. We can use --skip-mutation to bypass
   * the mutation phase, if we wanted to verify that a test program itself works.
   */
  protected Program fuzz() {
    String inputFile = getNextInputFilename();
    Program program = loadProgram(inputFile, null);
    if (program == null) {
      Log.errorAndQuit("Problem loading seed file.");
    }
    // Mutate the program.
    if (!Options.skipMutation) {
      timerMutation.start();
      program.mutateTheProgram();

      mutatedSuccessfully = program.updateRawDexFile();
      timerMutation.stop();
      if (!mutatedSuccessfully) {
        listener.handleMutationFail();
      }
    } else {
      Log.info("Skipping mutation stage as requested.");
      mutatedSuccessfully = true;
    }
    if (mutatedSuccessfully) {
      savedSuccessfully = saveProgram(program, getNextOutputFilename());
    }
    return program;
  }

  protected boolean safeToExecute() {
    return mutatedSuccessfully && savedSuccessfully;
  }

  protected void execute(Program program) {
    if (!safeToExecute()) {
      Log.errorAndQuit("Your Fuzzer subclass called execute() "
          + "without checking safeToExecute()!");
    }

    String programName = getNextOutputFilename();
    boolean verified = true;

    if (!Options.skipHostVerify && !Options.executeOnHost) {
      verified = goldenExecutor.verifyOnHost(programName);
      if (verified) {
        listener.handleSuccessfulHostVerification();
      }
    }

    if (verified) {
      boolean skipAnalysis = false;

      for (Executor executor : executors) {
        executor.reset();
        executor.prepareProgramForExecution(programName);
        executor.execute(programName);
        if (!executor.didTargetVerify()) {
          listener.handleFailedTargetVerification();
          skipAnalysis = true;
          break;
        }
        // Results are saved in the executors until they reset, usually at the
        // next iteration.
      }

      if (!skipAnalysis) {
        listener.handleSuccessfullyFuzzedFile(programName);
        analyseResults(program, programName);
      }
    }

    goldenExecutor.finishedWithProgramOnDevice();
    mutatedSuccessfully = false;
    savedSuccessfully = false;
  }

  /**
   * Checks if the different outputs we observed align with different architectures.
   */
  private boolean checkForArchitectureSplit(Map<String, List<Executor>> outputMap) {
    if (outputMap.size() != 2) {
      // Cannot have a two-way split if we don't have 2 kinds of output.
      return false;
    }

    Architecture[] architectures = new Architecture[2];
    int archIdx = 0;

    // For each kind of output we saw, make sure they all
    // came from the same architecture.
    for (List<Executor> executorList : outputMap.values()) {
      architectures[archIdx] = executorList.get(0).getArchitecture();
      for (int execIdx = 1; execIdx < executorList.size(); execIdx++) {
        if (executorList.get(execIdx).getArchitecture() != architectures[archIdx]) {
          // Not every executor with this output shared the same architecture.
          return false;
        }
      }
      archIdx++;
    }

    // Now make sure that the two outputs we saw were different architectures.
    if (architectures[0] == architectures[1]) {
      return false;
    }
    return true;
  }

  private boolean checkGoldenExecutorForSelfDivergence(String programName) {
    // Run golden executor multiple times, make sure it always produces
    // the same output, otherwise report that it is self-divergent.

    // TODO: Instead, produce a list of acceptable outputs, and see if the divergent
    // outputs of the backends fall within this set of outputs.
    String seenOutput = null;
    for (int i = 0; i < Options.divergenceRetry + 1; i++) {
      goldenExecutor.reset();
      goldenExecutor.execute(programName);
      String output = goldenExecutor.getResult().getFlattenedOutput();
      if (seenOutput == null) {
        seenOutput = output;
      } else if (!seenOutput.equals(output)) {
        return true;
      }
    }
    return false;
  }

  private void analyseResults(Program program, String programName) {
    // Check timeouts.
    // Construct two lists of executors, those who timed out, and those who did not.
    // Report if we had some timeouts.
    List<Executor> timedOut = new ArrayList<Executor>();
    List<Executor> didNotTimeOut = new ArrayList<Executor>();
    for (Executor executor : executors) {
      if (executor.getResult().isTimeout()) {
        timedOut.add(executor);
      } else {
        didNotTimeOut.add(executor);
      }
    }
    if (!timedOut.isEmpty()) {
      listener.handleTimeouts(timedOut, didNotTimeOut);
      // Do not bother reporting divergence information.
      return;
    }

    // Check divergences.
    // Construct a map {output1: [executor that produced output1, ...], output2: [...]}
    // If the map has more than one output, we had divergence, report it.
    Map<String, List<Executor>> outputMap = new HashMap<String, List<Executor>>();
    for (Executor executor : executors) {
      String output = executor.getResult().getFlattenedOutput();
      if (Options.dumpOutput) {
        listener.handleDumpOutput(
            executor.getResult().getFlattenedOutputWithNewlines(), executor);
      }
      if (outputMap.containsKey(output)) {
        outputMap.get(output).add(executor);
      } else {
        List<Executor> newList = new ArrayList<Executor>();
        newList.add(executor);
        outputMap.put(output, newList);
      }
    }

    if (outputMap.size() > 1) {
      // Report that we had divergence.
      listener.handleDivergences(outputMap);
      listener.handleMutations(program.getMutations());
      // If we found divergences, try running the "golden executor"
      // a few times in succession, to see if the output it produces is different
      // from run to run. If so, then we're probably executing something with either:
      //  a) randomness
      //  b) timing-dependent code
      //  c) threads
      // So we will not consider it a "true" divergence, but still useful?
      if (checkGoldenExecutorForSelfDivergence(programName)) {
        listener.handleSelfDivergence();
        return;
      }
      // If we found divergences, try checking if the differences
      // in outputs align with differences in architectures.
      // For example, if we have: {Output1: [ARM, ARM], Output2: [ARM64, ARM64]}
      if (checkForArchitectureSplit(outputMap)) {
        listener.handleArchitectureSplit();
      }
    } else {
      // No problems with execution.
      listener.handleSuccess(outputMap);
    }
  }

  private Program loadProgram(String inputName, List<Mutation> mutations) {
    Program program = null;
    try {
      DexRandomAccessFile input = new DexRandomAccessFile(inputName, "r");
      offsetTracker = new OffsetTracker();
      input.setOffsetTracker(offsetTracker);
      // Read the raw DexFile
      RawDexFile rawDexFile = new RawDexFile();
      timerDexInput.start();
      rawDexFile.read(input);
      timerDexInput.stop();
      input.close();
      // Create the program view.
      timerProgGen.start();
      program = new Program(rawDexFile, mutations, listener);
      timerProgGen.stop();
    } catch (FileNotFoundException e) {
      Log.errorAndQuit("Couldn't open a file called " + inputName);
    } catch (IOException e) {
      Log.errorAndQuit("IOException when trying to load a DEX test file!");
    }
    return program;
  }

  private boolean saveProgram(Program program, String outputName) {
    boolean success = false;

    try {
      // Write out the results of mutation.
      DexRandomAccessFile output = new DexRandomAccessFile(outputName, "rw");
      output.setOffsetTracker(offsetTracker);
      // Delete the contents of the file, in case it already existed.
      output.setLength(0);
      // Write out the file.
      timerDexOutput.start();
      program.writeRawDexFile(output);
      timerDexOutput.stop();
      // Recalculate the header info.
      timerChecksumCalc.start();
      program.updateRawDexFileHeader(output);
      timerChecksumCalc.stop();
      output.close();
      success = true;
    } catch (FileNotFoundException e) {
      Log.errorAndQuit("Couldn't open a file called " + outputName);
    } catch (IOException e) {
      Log.errorAndQuit("IOException when trying to save a DEX test file!");
    }
    return success;
  }
}
