/**
 * This file is part of Eclipse Steady.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * SPDX-FileCopyrightText: Copyright (c) 2018-2020 SAP SE or an SAP affiliate company and Eclipse Steady contributors
 */
package org.eclipse.steady.cg.soot;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import com.ibm.wala.util.graph.Graph;
import com.ibm.wala.util.graph.impl.SlowSparseNumberedGraph;

import org.apache.commons.configuration.Configuration;
import org.apache.logging.log4j.Logger;
import org.eclipse.steady.cg.CallgraphConstructException;
import org.eclipse.steady.cg.spi.ICallgraphConstructor;
import org.eclipse.steady.java.JavaConstructorId;
import org.eclipse.steady.java.JavaId;
import org.eclipse.steady.java.JavaMethodId;
import org.eclipse.steady.java.monitor.ClassVisitor;
import org.eclipse.steady.shared.json.model.Application;
import org.eclipse.steady.shared.util.StringUtil;
import org.eclipse.steady.shared.util.VulasConfiguration;

import soot.G;
import soot.MethodOrMethodContext;
import soot.PackManager;
import soot.Scene;
import soot.SootClass;
import soot.SootMethod;
import soot.jimple.infoflow.entryPointCreators.IEntryPointCreator;
import soot.jimple.toolkits.callgraph.CallGraph;
import soot.jimple.toolkits.callgraph.Edge;
import soot.options.Options;

/**
 * Callgraph constructor using soot framework; implementing the interface ICallgraphConstructor
 */
public class SootCallgraphConstructor implements ICallgraphConstructor {

  private static final Logger log = org.apache.logging.log4j.LogManager.getLogger();

  /** Constant <code>FRAMEWORK="soot"</code> */
  public static final String FRAMEWORK = "soot";

  private long buildTimeNano = -1;

  private long start_nanos = -1;

  /**
   * The context information of the application JAR to be analyzed
   */
  private Application appContext = null;

  private VulasConfiguration vulasConfiguration = null;

  protected String classpath = null;

  private String appClasspath = null;

  protected final List<SootMethod> entrypoints = new ArrayList<>();

  private final Set<org.eclipse.steady.shared.json.model.ConstructId> filteredEP = new HashSet<>();

  private CallGraph callgraph = null;

  /**
   * {@inheritDoc}
   *
   * Set the context of the application to be analyzed
   */
  public void setAppContext(Application _ctx) {
    this.appContext = _ctx;
  }

  /** {@inheritDoc} */
  public void setVulasConfiguration(VulasConfiguration _cfg) {
    this.vulasConfiguration = _cfg;
  }

  /**
   * <p>Getter for the field <code>appContext</code>.</p>
   *
   * @return a {@link org.eclipse.steady.shared.json.model.Application} object.
   */
  public Application getAppContext() {
    return this.appContext;
  }

  /**
   * <p>getFramework.</p>
   *
   * @return a {@link java.lang.String} object.
   */
  public String getFramework() {
    return SootCallgraphConstructor.FRAMEWORK;
  }

  /** {@inheritDoc} */
  public void setDepClasspath(String _dependenciesClasspath) {
    if (this.classpath != null) {
      this.classpath += System.getProperty("path.separator");
      this.classpath += _dependenciesClasspath;
    } else this.classpath = _dependenciesClasspath;
    SootCallgraphConstructor.log.info(
        "Add to soot classpath the dependencies: [" + this.classpath + "]");
  }

  /** {@inheritDoc} */
  public void setAppClasspath(String _cp) {
    if (this.classpath != null) {
      this.classpath += System.getProperty("path.separator");
      this.classpath += _cp;
    } else {
      this.classpath = _cp;
    }
    this.appClasspath = _cp;

    SootCallgraphConstructor.log.info(
        "Add to soot classpath the application: [" + this.classpath + "]");
  }

  /**
   * Returns the time required for building the call graph (in nanoseconds), or -1 if the construction did not finish.
   *
   * @return the time required for building the call graph (in nanoseconds)
   */
  public long getConstructionTime() {
    return this.buildTimeNano;
  }

  /**
   * Returns a human-readable description of the constructor's specific configuration.
   *
   * @return a {@link org.apache.commons.configuration.Configuration} object.
   */
  public Configuration getConstructorConfiguration() {
    return this.vulasConfiguration
        .getConfiguration()
        .subset(SootConfiguration.SOOT_CONFIGURATION_SETTINGS);
  }

  /*
   * First resets and then configures Soot's Options
   */

  /**
   * <p>sootSetup.</p>
   */
  protected void sootSetup() {

    // start with a clean run of Soot
    G.v().resetSpark();
    G.reset();

    boolean verbose =
        this.vulasConfiguration.getConfiguration().getBoolean(SootConfiguration.SOOT_VERBOSE);

    // set default excluded list to empty list
    // Options.v().set_include_all(true);

    String excludedPackages =
        this.vulasConfiguration.getConfiguration().getString(SootConfiguration.SOOT_EXCLUSIONS);
    List<String> excludedList = Arrays.asList(excludedPackages.split(";"));
    Options.v().set_exclude(excludedList);

    // add the rt.jar and jce.jar to the classpath; WALA does this silently in the background
    Options.v().set_prepend_classpath(true);

    // Read from soot-cfg.properties
    Options.v()
        .set_allow_phantom_refs(
            this.vulasConfiguration
                .getConfiguration()
                .getBoolean(SootConfiguration.SOOT_ALLOW_PHANTOM));
    Options.v().set_verbose(verbose);
    Options.v()
        .set_app(
            this.vulasConfiguration.getConfiguration().getBoolean(SootConfiguration.SOOT_APP_MODE));
    Options.v().set_whole_program(true);
    Options.v().setPhaseOption("cg", "safe-forname:" + false);

    // additional options for easy debugging
    Options.v().set_keep_line_number(true);
    Options.v().set_throw_analysis(Options.throw_analysis_unit);
    Options.v().setPhaseOption("cg", "verbose:" + verbose);
    Options.v().set_debug(verbose);
    Options.v().set_debug_resolver(verbose);

    // do not release bodies after running the packs
    Options.v().set_output_format(Options.output_format_none);

    // with this option we get only method signatures but not their bodies
    Options.v()
        .set_no_bodies_for_excluded(
            this.vulasConfiguration
                .getConfiguration()
                .getBoolean(SootConfiguration.SOOT_NOBODY_FOR_X));

    if (this.vulasConfiguration.getConfiguration().getBoolean(SootConfiguration.SOOT_SPARK)) {
      String s = "on";
      if (this.vulasConfiguration.getConfiguration().getBoolean(SootConfiguration.SOOT_SPARK_OTF))
        s += ",on-fly-cg:true";
      else s += ",on-fly-cg:false";
      if (this.vulasConfiguration.getConfiguration().getBoolean(SootConfiguration.SOOT_SPARK_VTA))
        s += ",vta:true";
      else s += ",vta:false";
      if (this.vulasConfiguration.getConfiguration().getBoolean(SootConfiguration.SOOT_SPARK_RTA))
        s += ",rta:true";
      else s += ",rta:false";
      SootCallgraphConstructor.log.info("Enabled cg.spark with settings [" + s + "]");
      Options.v().setPhaseOption("cg.spark", s);
    }

    ArrayList<String> processDirs = new ArrayList<>();
    final StringTokenizer tokenizer =
        new StringTokenizer(this.appClasspath, System.getProperty("path.separator"));
    while (tokenizer.hasMoreTokens()) {
      processDirs.add(tokenizer.nextToken());
    }

    Options.v().set_process_dir(processDirs);
    Options.v().set_soot_classpath(this.classpath);
  }

  /**
   * Gets the SootMethod that correspond to the given constructs
   *
   * @param _constructs a {@link java.util.Set} object.
   * @throws org.eclipse.steady.cg.CallgraphConstructException
   */
  protected void sootMethods4entrypoints(
      Set<org.eclipse.steady.shared.json.model.ConstructId> _constructs)
      throws CallgraphConstructException {

    SootMethod method = null;
    SootClass ep = null;
    // No entrypoints set, search for a main (if existing)
    if (_constructs.isEmpty()) {
      SootCallgraphConstructor.log.info(
          "No customized entrypoints set; search for main as default entry point");
      Iterator<SootClass> classes = Scene.v().getApplicationClasses().iterator();
      try {
        while (classes.hasNext()) {
          ep = classes.next();
          method = ep.getMethodByName("main");
          if (method != null) {
            this.filteredEP.add(getCid(method));
            this.entrypoints.add(method);
            break;
          }
        }
      } catch (Exception e) {
        SootCallgraphConstructor.log.error(
            "Error while searching for main method in class [" + ep + "]: " + e.getMessage());
      }
      if (this.entrypoints.isEmpty())
        throw new CallgraphConstructException(
            "No main method found that can be used as entry point", null);
    }

    // Use entrypoints passed as arg
    else {

      Iterator<SootMethod> iter = null;
      for (org.eclipse.steady.shared.json.model.ConstructId cid : _constructs) {

        final JavaId jcid = (JavaId) JavaId.toCoreType(cid);

        // when it's a java method
        if (jcid instanceof org.eclipse.steady.java.JavaMethodId) {
          JavaMethodId mid = (JavaMethodId) jcid;
          try {
            ep = Scene.v().getSootClass(mid.getDefinitionContext().getQualifiedName());

            // Loop over methods to find the one :/
            iter = ep.methodIterator();
            while (iter.hasNext()) {
              method = iter.next();
              if (method.isConcrete()) {
                // Compare the JavaMethodId qname
                if (getCid(method).getQname().equals(mid.getQualifiedName())) {
                  this.filteredEP.add(cid);
                  this.entrypoints.add(method);
                  break;
                }
              }
            }
          } catch (Exception e) {
            SootCallgraphConstructor.log.error(
                "Error while searching for method " + mid.toString() + ": " + e.getMessage());
          }
        }
        // when it's a java object constructor
        else if (jcid instanceof org.eclipse.steady.java.JavaConstructorId) {
          JavaConstructorId jconsid = (JavaConstructorId) jcid;
          try {
            ep = Scene.v().getSootClass(jconsid.getDefinitionContext().getQualifiedName());
            // Loop over constructors to find the one :/
            iter = ep.methodIterator();
            while (iter.hasNext()) {
              method = iter.next();
              if (method.isConstructor()) {
                // Compare the JavaConstructorId qname
                if (getCid(method).getQname().equals(jconsid.getQualifiedName())) {
                  this.filteredEP.add(cid);
                  this.entrypoints.add(method);
                  break;
                }
              }
            }
          } catch (Exception e) {
            SootCallgraphConstructor.log.error(
                "Error while searching for method " + jcid.toString() + ": " + e.getMessage());
          }
        }
      }
    }
  }

  /**
   * {@inheritDoc}
   *
   * Filter and find all entrypoints in Scene
   */
  public void setEntrypoints(Set<org.eclipse.steady.shared.json.model.ConstructId> _constructs)
      throws CallgraphConstructException {

    start_nanos = System.nanoTime();

    // setup soot
    this.sootSetup();

    // load the all necessary classes
    Scene.v().loadNecessaryClasses();

    // determine the entrypoints
    this.sootMethods4entrypoints(_constructs);

    if (!this.entrypoints.isEmpty()) {
      Scene.v().setEntryPoints(createEntryPoint4Soot(this.entrypoints));

      SootCallgraphConstructor.log.info("[" + this.entrypoints.size() + "] entry points set");
    } else {
      throw new CallgraphConstructException(
          "No entry points could be set, which will not allow to build the callgraph", null);
    }
  }

  private ArrayList<SootMethod> createEntryPoint4Soot(Collection<SootMethod> selectedEntrypoints) {
    String slcEntrypointGenerator =
        this.vulasConfiguration
            .getConfiguration()
            .getString(SootConfiguration.SOOT_ENTRYPOINT_GENERATOR);

    if (slcEntrypointGenerator.toLowerCase().equals("none")) {
      return new ArrayList<>(selectedEntrypoints);
    }

    ArrayList<String> methodsToCall = new ArrayList<>();
    for (SootMethod sm : selectedEntrypoints) {
      methodsToCall.add(sm.getSignature());
    }

    try {
      final Class cls = Class.forName(slcEntrypointGenerator);
      Constructor constructor = cls.getDeclaredConstructor(Collection.class);
      IEntryPointCreator entryPointCreator =
          (IEntryPointCreator) constructor.newInstance(methodsToCall);
      SootMethod dummyMain = entryPointCreator.createDummyMain();
      ArrayList<SootMethod> generatedEntrypoint = new ArrayList<>();
      generatedEntrypoint.add(dummyMain);
      return generatedEntrypoint;

    } catch (Throwable e) {
      SootCallgraphConstructor.log.error(
          "Error while creating entrypoint generator of class ["
              + slcEntrypointGenerator
              + "]: "
              + e.getMessage(),
          e);
    }

    return new ArrayList<>();
  }

  /**
   * {@inheritDoc}
   *
   * Read all configurations and parse command line options, and then build callgraph based on these properties
   */
  public void buildCallgraph(boolean _policy) throws CallgraphConstructException {
    SootCallgraphConstructor.log.info(
        "Starting call graph construction for " + this.appContext.toString(false));

    try {
      PackManager.v().runPacks();

      this.callgraph = Scene.v().getCallGraph();
      this.buildTimeNano = System.nanoTime() - start_nanos;
      SootCallgraphConstructor.log.info(
          "Construction completed in "
              + StringUtil.nanoToFlexDurationString(this.buildTimeNano)
              + ", call graph has ["
              + callgraph.size()
              + "] edges]");
      checkEntrypoints(_policy);
    } catch (CallgraphConstructException e) {
      SootCallgraphConstructor.log.error("Error building call graph: " + e.getMessage());
      throw new CallgraphConstructException("Error building call graph", e);
    }
  }

  /**
   * check whether all entrypoints are existing in callgraph
   *
   * @throws CallgraphConstructException if no callgraph could be built
   */
  private void checkEntrypoints(boolean _policy) throws CallgraphConstructException {
    final HashSet<SootMethod> check_ep = new HashSet<>();
    check_ep.addAll(this.entrypoints);

    Iterator<MethodOrMethodContext> src_nodes = callgraph.sourceMethods();
    SootMethod method = null;
    MethodOrMethodContext src_node = null;
    Iterator<Edge> edges = null;
    while (src_nodes.hasNext()) {
      src_node = src_nodes.next();
      method = src_node.method();
      if (this.entrypoints.contains(method)) check_ep.remove(method);
      edges = this.callgraph.edgesOutOf(src_node);
      while (edges.hasNext()) {
        method = edges.next().tgt();
        if (this.entrypoints.contains(method)) check_ep.remove(method);
      }
    }
    int diff = check_ep.size();

    if (_policy && diff != 0) {
      for (SootMethod m : check_ep)
        SootCallgraphConstructor.log.warn("[ " + m.getSignature() + " ] is missing");
      throw new CallgraphConstructException(
          "Strict policy applied; terminating as there are ["
              + diff
              + "] entry points missing in call graph",
          null);
    }
    // Throw exception if number of missing EPs exceeds threshold
    if (this.entrypoints.size() - diff == 0)
      throw new CallgraphConstructException(
          "[0/" + this.entrypoints.size() + "] entry points found in call graph", null);

    // Print warning for missing entry points
    if (diff > 0) {
      SootCallgraphConstructor.log.warn(
          "There should be "
              + this.entrypoints.size()
              + " entrypoints set; but "
              + diff
              + " entrypoints missing in the call graph");
      for (SootMethod m : check_ep)
        SootCallgraphConstructor.log.warn("[ " + m.getSignature() + " ] is missing");
    } else {
      SootCallgraphConstructor.log.info(
          "All [" + this.entrypoints.size() + "] entry points existing in the call graph");
    }
  }

  /**
   * Given a SootMethod, return its ConstructId
   *
   * @param _method the {@link soot.SootMethod} to compute the ConstructId for
   * @return computed ConstructId
   */
  private static org.eclipse.steady.shared.json.model.ConstructId getCid(SootMethod _method) {
    String qname = null;
    org.eclipse.steady.shared.json.model.ConstructId cid = null;
    String signature = _method.getSignature();
    if (_method.getName().equals("<clinit>")) {
      qname = signature.substring(1, _method.getSignature().indexOf(":"));
      cid = JavaId.toSharedType(JavaId.parseClassQName(qname).getClassInit());
    } else {
      if (_method.isConstructor()) qname = signature.substring(1, signature.indexOf(":"));
      else qname = signature.substring(1, signature.indexOf(":")) + "." + _method.getName();
      signature = _method.getSubSignature();
      qname += signature.substring(signature.indexOf("("), signature.indexOf(")") + 1);
      qname = ClassVisitor.removeParameterQualification(qname);
      if (_method.isConstructor()) cid = JavaId.toSharedType(JavaId.parseConstructorQName(qname));
      else cid = JavaId.toSharedType(JavaId.parseMethodQName(qname));
    }
    return cid;
  }

  /**
   * Normalizing a soot callgraph to a general graph represented by ConstructId
   *
   * @return a {@link com.ibm.wala.util.graph.Graph} object.
   */
  public Graph<org.eclipse.steady.shared.json.model.ConstructId> getCallgraph() {
    final Graph<org.eclipse.steady.shared.json.model.ConstructId> graph =
        SlowSparseNumberedGraph.make();

    if (this.callgraph != null) {
      int edges_no = 0;
      org.eclipse.steady.shared.json.model.ConstructId src_cid = null, tgt_cid = null;
      MethodOrMethodContext src_node = null;
      Iterator<Edge> edges = null;

      final Iterator<MethodOrMethodContext> src_nodes = callgraph.sourceMethods();
      while (src_nodes.hasNext()) {
        src_node = src_nodes.next();
        src_cid = getCid(src_node.method());
        graph.addNode(src_cid);

        // add edges
        edges = this.callgraph.edgesOutOf(src_node);
        while (edges.hasNext()) {
          tgt_cid = getCid(edges.next().tgt());
          graph.addNode(tgt_cid);
          if (!graph.hasEdge(src_cid, tgt_cid)) {
            graph.addEdge(src_cid, tgt_cid);
            edges_no++;
          }
        }
      }

      SootCallgraphConstructor.log.info(
          "Normalized call graph has ["
              + graph.getNumberOfNodes()
              + " nodes] (with distinct ConstructId) and ["
              + edges_no
              + "] edges");
    }
    // No callgraph exists
    else {
      throw new IllegalStateException("There exists no call graph");
    }
    return graph;
  }

  /**
   * <p>Getter for the field <code>entrypoints</code>.</p>
   *
   * @return a {@link java.util.Set} object.
   */
  public Set<org.eclipse.steady.shared.json.model.ConstructId> getEntrypoints() {
    return this.filteredEP;
  }

  /** {@inheritDoc} */
  public void setExcludePackages(String _packages) {
    // Overwrite configuration (if requested)
    if (_packages != null && !_packages.equals(""))
      this.vulasConfiguration.setProperty(SootConfiguration.SOOT_EXCLUSIONS, _packages);
    SootCallgraphConstructor.log.info(
        "Set packages to be excluded [ "
            + this.vulasConfiguration
                .getConfiguration()
                .getString(SootConfiguration.SOOT_EXCLUSIONS)
            + " ]");
  }
}
