/*
 * Copyright (C) 2015 The Pennsylvania State University and the University of Wisconsin
 * Systems and Internet Infrastructure Security Laboratory
 *
 * Author: Damien Octeau
 *
 * 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 edu.psu.cse.siis.ic3;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParserException;

import soot.PackManager;
import soot.Scene;
import soot.SootClass;
import soot.SootMethod;
import soot.Transform;
import soot.Value;
import soot.jimple.StaticFieldRef;
import soot.jimple.infoflow.android.data.AndroidMethod;
import soot.jimple.infoflow.android.manifest.ProcessManifest;
import soot.options.Options;
import edu.psu.cse.siis.coal.Analysis;
import edu.psu.cse.siis.coal.AnalysisParameters;
import edu.psu.cse.siis.coal.FatalAnalysisException;
import edu.psu.cse.siis.coal.PropagationSceneTransformer;
import edu.psu.cse.siis.coal.PropagationSceneTransformerFilePrinter;
import edu.psu.cse.siis.coal.SymbolFilter;
import edu.psu.cse.siis.coal.arguments.ArgumentValueManager;
import edu.psu.cse.siis.coal.arguments.MethodReturnValueManager;
import edu.psu.cse.siis.coal.field.transformers.FieldTransformerManager;
import edu.psu.cse.siis.ic3.db.SQLConnection;
import edu.psu.cse.siis.ic3.manifest.ManifestPullParser;

public class Ic3Analysis extends Analysis<Ic3CommandLineArguments> {
  private static final String INTENT = "android.content.Intent";
  private static final String INTENT_FILTER = "android.content.IntentFilter";
  private static final String BUNDLE = "android.os.Bundle";
  private static final String COMPONENT_NAME = "android.content.ComponentName";
  private static final String ACTIVITY = "android.app.Activity";

  private static final String[] frameworkClassesArray = { INTENT, INTENT_FILTER, BUNDLE,
      COMPONENT_NAME, ACTIVITY };
  protected static final List<String> frameworkClasses = Arrays.asList(frameworkClassesArray);

  private final Logger logger = LoggerFactory.getLogger(getClass());

  private Ic3Data.Application.Builder ic3Builder;
  private Map<String, Ic3Data.Application.Component.Builder> componentNameToBuilderMap;

  protected String outputDir;
  protected Writer writer;
  protected ManifestPullParser detailedManifest;
  protected Map<String, Integer> componentToIdMap;
  protected SetupApplication setupApplication;
  protected String packageName;

  @Override
  protected void registerFieldTransformerFactories(Ic3CommandLineArguments commandLineArguments) {
    Timers.v().totalTimer.start();
    FieldTransformerManager.v().registerDefaultFieldTransformerFactories();
  }

  @Override
  protected void registerArgumentValueAnalyses(Ic3CommandLineArguments commandLineArguments) {
    ArgumentValueManager.v().registerDefaultArgumentValueAnalyses();
    ArgumentValueManager.v().registerArgumentValueAnalysis("classType",
        new ClassTypeValueAnalysis());
    ArgumentValueManager.v().registerArgumentValueAnalysis("authority",
        new AuthorityValueAnalysis());
    ArgumentValueManager.v().registerArgumentValueAnalysis("Set<authority>",
        new AuthorityValueAnalysis());
    ArgumentValueManager.v().registerArgumentValueAnalysis("path", new PathValueAnalysis());
    ArgumentValueManager.v().registerArgumentValueAnalysis("Set<path>", new PathValueAnalysis());
  }

  @Override
  protected void registerMethodReturnValueAnalyses(Ic3CommandLineArguments commandLineArguments) {
    MethodReturnValueManager.v().registerDefaultMethodReturnValueAnalyses();
  }

  @Override
  protected void initializeAnalysis(Ic3CommandLineArguments commandLineArguments)
      throws FatalAnalysisException {
    long startTime = System.currentTimeMillis() / 1000;
    outputDir = commandLineArguments.getOutput();

    prepareManifestFile(commandLineArguments);

    if (commandLineArguments.getProtobufDestination() != null) {
      ic3Builder = Ic3Data.Application.newBuilder();
      ic3Builder.setAnalysisStart(startTime);
      if (commandLineArguments.getSample() != null) {
        ic3Builder.setSample(commandLineArguments.getSample());
      }
      componentNameToBuilderMap = detailedManifest.populateProtobuf(ic3Builder);
    } else if (commandLineArguments.getDb() != null) {
      SQLConnection.init(commandLineArguments.getDbName(), commandLineArguments.getDb(),
          commandLineArguments.getSsh(), commandLineArguments.getDbLocalPort());
      componentToIdMap = detailedManifest.writeToDb(false);
    }

    Timers.v().mainGeneration.start();
    setupApplication =
        new SetupApplication(commandLineArguments.getManifest(), commandLineArguments.getInput(),
            commandLineArguments.getClasspath());

    Map<String, Set<String>> callBackMethods;

    Set<String> entryPointClasses = null;
    if (detailedManifest == null) {
      ProcessManifest manifest;
      try {
        manifest = new ProcessManifest(commandLineArguments.getManifest());
        entryPointClasses = manifest.getEntryPointClasses();
        packageName = manifest.getPackageName();
      } catch (IOException | XmlPullParserException e) {
        throw new FatalAnalysisException("Could not process manifest file "
            + commandLineArguments.getManifest() + ": " + e);
      }
    } else {
      entryPointClasses = detailedManifest.getEntryPointClasses();
      packageName = detailedManifest.getPackageName();
    }

    try {
      callBackMethods =
          setupApplication.calculateSourcesSinksEntrypoints(new HashSet<AndroidMethod>(),
              new HashSet<AndroidMethod>(), packageName, entryPointClasses);
    } catch (IOException e) {
      logger.error("Could not calculate entry points", e);
      throw new FatalAnalysisException();
    }
    Timers.v().mainGeneration.end();

    Timers.v().misc.start();

    // Application package name is now known.
    ArgumentValueManager.v().registerArgumentValueAnalysis("context",
        new ContextValueAnalysis(packageName));
    AndroidMethodReturnValueAnalyses.registerAndroidMethodReturnValueAnalyses(packageName);

    if (outputDir != null && packageName != null) {
      String outputFile = String.format("%s/%s.csv", outputDir, packageName);

      try {
        writer = new BufferedWriter(new FileWriter(outputFile, false));
      } catch (IOException e1) {
        logger.error("Could not open file " + outputFile, e1);
      }
    }

    // reset Soot:
    soot.G.reset();

    Map<SootMethod, Set<String>> entryPointMap =
        commandLineArguments.computeComponents() ? new HashMap<SootMethod, Set<String>>() : null;
    addSceneTransformer(entryPointMap);

    if (commandLineArguments.computeComponents()) {
      addEntryPointMappingSceneTransformer(entryPointClasses, callBackMethods, entryPointMap);
    }

    Options.v().set_no_bodies_for_excluded(true);
    Options.v().set_allow_phantom_refs(true);
    Options.v().set_output_format(Options.output_format_none);
    Options.v().set_whole_program(true);
    Options.v().set_soot_classpath(
        commandLineArguments.getInput() + File.pathSeparator + commandLineArguments.getClasspath());
    Options.v().set_ignore_resolution_errors(true);
    Options.v().set_process_dir(frameworkClasses);

    Options.v().setPhaseOption("cg.spark", "on");

    // do not merge variables (causes problems with PointsToSets)
    Options.v().setPhaseOption("jb.ulp", "off");

    Options.v().setPhaseOption("jb.uce", "remove-unreachable-traps:true");

    Options.v().setPhaseOption("cg", "trim-clinit:false");
    Options.v().set_prepend_classpath(true);

    if (AnalysisParameters.v().useShimple()) {
      Options.v().set_via_shimple(true);
      Options.v().set_whole_shimple(true);
    }

    Options.v().set_src_prec(Options.src_prec_java);
    Timers.v().misc.end();

    Timers.v().classLoading.start();
    for (String frameworkClass : frameworkClasses) {
      SootClass c = Scene.v().loadClassAndSupport(frameworkClass);
      Scene.v().forceResolve(frameworkClass, SootClass.BODIES);
      c.setApplicationClass();
    }

    Scene.v().loadNecessaryClasses();
    Timers.v().classLoading.end();

    Timers.v().entryPointMapping.start();
    Scene.v().setEntryPoints(
        Collections.singletonList(setupApplication.getEntryPointCreator().createDummyMain()));
    Timers.v().entryPointMapping.end();
  }

  protected void prepareManifestFile(Ic3CommandLineArguments commandLineArguments) {
    if (commandLineArguments.getDb() != null
        || commandLineArguments.getProtobufDestination() != null) {
      detailedManifest = new ManifestPullParser();
      detailedManifest.loadManifestFile(commandLineArguments.getManifest());
    }
  }

  @Override
  protected void setApplicationClasses(Ic3CommandLineArguments commandLineArguments)
      throws FatalAnalysisException {
    AnalysisParameters.v().addAnalysisClasses(
        computeAnalysisClasses(commandLineArguments.getInput()));
    AnalysisParameters.v().addAnalysisClasses(frameworkClasses);
  }

  @Override
  protected void handleFatalAnalysisException(Ic3CommandLineArguments commandLineArguments,
      FatalAnalysisException exception) {
    logger.error("Could not process application " + packageName, exception);

    if (outputDir != null && packageName != null) {
      try {
        if (writer == null) {
          String outputFile = String.format("%s/%s.csv", outputDir, packageName);

          writer = new BufferedWriter(new FileWriter(outputFile, false));
        }

        writer.write(commandLineArguments.getInput() + " -1\n");
        writer.close();
      } catch (IOException e1) {
        logger.error("Could not write to file after failure to process application", e1);
      }
    }
  }

  @Override
  protected void processResults(Ic3CommandLineArguments commandLineArguments)
      throws FatalAnalysisException {
    System.out.println("\n*****Manifest*****");
    System.out.println(detailedManifest.toString());

    if (commandLineArguments.getProtobufDestination() != null) {
      ProtobufResultProcessor resultProcessor = new ProtobufResultProcessor();
      try {
        resultProcessor.processResult(packageName, ic3Builder,
            commandLineArguments.getProtobufDestination(), commandLineArguments.binary(),
            componentNameToBuilderMap, AnalysisParameters.v().getAnalysisClasses().size(), writer);
      } catch (IOException e) {
        logger.error("Could not process analysis results", e);
        throw new FatalAnalysisException();
      }
    } else {
      ResultProcessor resultProcessor = new ResultProcessor();
      try {
        resultProcessor.processResult(commandLineArguments.getDb() != null, packageName,
            componentToIdMap, AnalysisParameters.v().getAnalysisClasses().size(), writer);
      } catch (IOException | SQLException e) {
        logger.error("Could not process analysis results", e);
        throw new FatalAnalysisException();
      }
    }
  }

  @Override
  protected void finalizeAnalysis(Ic3CommandLineArguments commandLineArguments) {
  }

  protected void addSceneTransformer(Map<SootMethod, Set<String>> entryPointMap) {
    Ic3ResultBuilder resultBuilder = new Ic3ResultBuilder();
    resultBuilder.setEntryPointMap(entryPointMap);
    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
    String debugDirPath = System.getProperty("user.home") + File.separator + "debug";
    File debugDir = new File(debugDirPath);
    if (!debugDir.exists()) {
      debugDir.mkdir();
    }

    String fileName = dateFormat.format(new Date()) + ".txt";
    String debugFilename = debugDirPath + File.separator + fileName;

    String pack = AnalysisParameters.v().useShimple() ? "wstp" : "wjtp";
    Transform transform =
        new Transform(pack + ".ifds", new PropagationSceneTransformer(resultBuilder,
            new PropagationSceneTransformerFilePrinter(debugFilename, new SymbolFilter() {

              @Override
              public boolean filterOut(Value symbol) {
                return symbol instanceof StaticFieldRef
                    && ((StaticFieldRef) symbol).getField().getDeclaringClass().getName()
                        .startsWith("android.provider");
              }
            })));
    if (PackManager.v().getPack(pack).get(pack + ".ifds") == null) {
      PackManager.v().getPack(pack).add(transform);
    } else {
      Iterator<?> it = PackManager.v().getPack(pack).iterator();
      while (it.hasNext()) {
        Object current = it.next();
        if (current instanceof Transform
            && ((Transform) current).getPhaseName().equals(pack + ".ifds")) {
          it.remove();
          break;
        }

      }
      PackManager.v().getPack(pack).add(transform);
    }
  }

  protected void addEntryPointMappingSceneTransformer(Set<String> entryPointClasses,
      Map<String, Set<String>> entryPointMapping, Map<SootMethod, Set<String>> entryPointMap) {
    String pack = AnalysisParameters.v().useShimple() ? "wstp" : "wjtp";

    Transform transform =
        new Transform(pack + ".epm", new EntryPointMappingSceneTransformer(entryPointClasses,
            entryPointMapping, entryPointMap));
    if (PackManager.v().getPack(pack).get(pack + ".epm") == null) {
      PackManager.v().getPack(pack).add(transform);
    } else {
      Iterator<?> it = PackManager.v().getPack(pack).iterator();
      while (it.hasNext()) {
        Object current = it.next();
        if (current instanceof Transform
            && ((Transform) current).getPhaseName().equals(pack + ".epm")) {
          it.remove();
          break;
        }

      }
      PackManager.v().getPack(pack).add(transform);
    }
  }
}