package edu.psu.cse.siis.ic3;

import java.io.IOException;
import java.io.Writer;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import soot.Scene;
import soot.SootMethod;
import soot.Type;
import soot.Unit;
import soot.jimple.InvokeExpr;
import soot.jimple.Stmt;
import edu.psu.cse.siis.coal.AnalysisParameters;
import edu.psu.cse.siis.coal.Constants;
import edu.psu.cse.siis.coal.Model;
import edu.psu.cse.siis.coal.PropagationTimers;
import edu.psu.cse.siis.coal.Result;
import edu.psu.cse.siis.coal.Results;
import edu.psu.cse.siis.coal.arguments.Argument;
import edu.psu.cse.siis.coal.field.values.FieldValue;
import edu.psu.cse.siis.coal.field.values.ScalarFieldValue;
import edu.psu.cse.siis.coal.field.values.TopFieldValue;
import edu.psu.cse.siis.coal.values.BasePropagationValue;
import edu.psu.cse.siis.coal.values.BottomPropagationValue;
import edu.psu.cse.siis.coal.values.PathValue;
import edu.psu.cse.siis.coal.values.PropagationValue;
import edu.psu.cse.siis.coal.values.TopPropagationValue;
import edu.psu.cse.siis.ic3.db.DbConnection;
import edu.psu.cse.siis.ic3.db.SQLConnection;
import edu.psu.cse.siis.ic3.manifest.ManifestComponent;
import edu.psu.cse.siis.ic3.manifest.ManifestData;
import edu.psu.cse.siis.ic3.manifest.ManifestIntentFilter;

public class ResultProcessor {
  private final Logger logger = LoggerFactory.getLogger(getClass());

  private static final String ENTRY_POINT_INTENT = "<INTENT>";

  private final int[] preciseNonLinking = { 0, 0, 0, 0 };
  private final int[] preciseLinking = { 0, 0, 0, 0 };
  private final int[] imprecise = { 0, 0, 0, 0, 0 };
  private final int[] top = { 0, 0, 0 };
  private final int[] bottom = { 0, 0, 0 };
  private final int[] nonexistent = { 0, 0, 0 };
  private final int[] preciseFieldValueCount = { 0, 0, 0 };
  private final int[] partiallyPreciseFieldValueCount = { 0, 0, 0 };
  private final int[] impreciseFieldValueCount = { 0, 0, 0 };
  private int intentWithData = 0;
  private int providerArgument = 0;

  public void processResult(boolean writeToDb, String appName,
      Map<String, Integer> componentToIdMap, int analysisClassesCount, Writer writer)
      throws IOException, SQLException {
    for (Result result : Results.getResults()) {
      ((Ic3Result) result).dump();
      analyzeResult(result);
      if (writeToDb) {
        writeResultToDb(result, componentToIdMap);
      }
    }

    if (writeToDb) {
      SQLConnection.closeConnection();
    }

    Timers.v().totalTimer.end();

    String statistics =
        appName + " " + analysisClassesCount + " " + PropagationTimers.v().reachableMethods + " "
            + PropagationTimers.v().reachableStatements + " " + preciseNonLinking[0] + " "
            + preciseNonLinking[3] + " " + preciseNonLinking[1] + " " + preciseNonLinking[2] + " "
            + preciseLinking[0] + " " + preciseLinking[3] + " " + preciseLinking[1] + " "
            + preciseLinking[2] + " " + imprecise[0] + " " + imprecise[3] + " " + imprecise[1]
            + " " + imprecise[2] + " " + bottom[0] + " " + bottom[1] + " " + bottom[2] + " "
            + top[0] + " " + top[1] + " " + top[2] + " " + nonexistent[0] + " " + nonexistent[1]
            + " " + nonexistent[2] + " " + providerArgument + " " + imprecise[4] + " "
            + PropagationTimers.v().pathValues + " " + PropagationTimers.v().separatePathValues
            + " " + PropagationTimers.v().modelParsing.getTime() + " "
            + Timers.v().mainGeneration.getTime() + " " + Timers.v().entryPointMapping.getTime()
            + " " + Timers.v().classLoading.getTime() + " "
            + PropagationTimers.v().problemGeneration.getTime() + " "
            + PropagationTimers.v().ideSolution.getTime() + " "
            + PropagationTimers.v().valueComposition.getTime() + " "
            + PropagationTimers.v().resultGeneration.getTime() + " "
            + (PropagationTimers.v().soot.getTime() - PropagationTimers.v().totalTimer.getTime())
            + " " + (Timers.v().misc.getTime() + PropagationTimers.v().misc.getTime()) + " "
            /* + PropagationTimers.v().argumentValueTime + " " */+ Timers.v().totalTimer.getTime()
            + "\n";

    if (logger.isInfoEnabled()) {
      logger.info(statistics);
    }

    if (writer != null) {
      writer.write(statistics);
      writer.close();
    }
  }

  @SuppressWarnings("unchecked")
  private void writeResultToDb(Result result, Map<String, Integer> componentToIdMap)
      throws SQLException {
    for (Map.Entry<Unit, Map<Integer, Object>> entry : result.getResults().entrySet()) {
      Unit unit = entry.getKey();

      Argument[] arguments = Model.v().getArgumentsForQuery((Stmt) unit);
      Map<SootMethod, Set<String>> entryPointMap = ((Ic3Result) result).getEntryPointMap();

      if (arguments != null) {
        SootMethod method = AnalysisParameters.v().getIcfg().getMethodOf(unit);
        int unitId = getIdForUnit(unit, method);
        Map<String, Object> valueMap = new HashMap<>(arguments.length);
        Map<Integer, Object> argnumToValueMap = entry.getValue();

        for (Argument argument : arguments) {
          valueMap.put(argument.getProperty("valueType"),
              argnumToValueMap.get(argument.getArgnum()[0]));
        }

        String className = method.getDeclaringClass().getName();
        String methodSignature = method.getSignature();
        if (valueMap.containsKey("activity")) {
          DbConnection.insertIntentAtExitPoint(className, methodSignature, unitId,
              (BasePropagationValue) valueMap.get("activity"),
              edu.psu.cse.siis.ic3.db.Constants.ComponentShortType.ACTIVITY, null, null,
              entryPointMap.get(method), componentToIdMap);
        } else if (valueMap.containsKey("service")) {
          DbConnection.insertIntentAtExitPoint(className, methodSignature, unitId,
              (BasePropagationValue) valueMap.get("service"),
              edu.psu.cse.siis.ic3.db.Constants.ComponentShortType.SERVICE, null, null,
              entryPointMap.get(method), componentToIdMap);
        } else if (valueMap.containsKey("receiver")) {
          DbConnection.insertIntentAtExitPoint(className, methodSignature, unitId,
              (BasePropagationValue) valueMap.get("receiver"),
              edu.psu.cse.siis.ic3.db.Constants.ComponentShortType.RECEIVER,
              (Set<String>) valueMap.get("permission"), null, entryPointMap.get(method),
              componentToIdMap);
        } else if (valueMap.containsKey("intentFilter")) {
          insertDynamicReceiver((Set<String>) valueMap.get("permission"),
              (Set<String>) valueMap.get("receiverType"),
              (BasePropagationValue) valueMap.get("intentFilter"), method, unit);
        } else if (valueMap.containsKey("provider")) {
          DbConnection.insertIntentAtExitPoint(className, methodSignature, unitId,
              (BasePropagationValue) valueMap.get("provider"),
              edu.psu.cse.siis.ic3.db.Constants.ComponentShortType.PROVIDER, null, null,
              entryPointMap.get(method), componentToIdMap);
        } else if (valueMap.containsKey("authority")) {
          DbConnection.insertIntentAtExitPoint(className, methodSignature, unitId,
              getUriValueForAuthorities((Set<String>) valueMap.get("authority")),
              edu.psu.cse.siis.ic3.db.Constants.ComponentShortType.PROVIDER, null, null,
              entryPointMap.get(method), componentToIdMap);
        } else if (valueMap.containsKey("pendingIntent")) {
          BasePropagationValue basePropagationValue =
              (BasePropagationValue) valueMap.get("pendingIntent");
          String targetType =
              basePropagationValue instanceof PropagationValue ? (String) ((PropagationValue) basePropagationValue)
                  .getValuesForField("targetType").iterator().next().getValue()
                  : null;
          Set<String> permissions = (Set<String>) valueMap.get("permission");
          if (targetType != null) {
            DbConnection.insertIntentAtExitPoint(className, methodSignature, unitId,
                basePropagationValue, targetType, permissions, null, entryPointMap.get(method),
                componentToIdMap);
          } else {
            for (String target : Arrays.asList("a", "r", "s")) {
              DbConnection.insertIntentAtExitPoint(className, methodSignature, unitId,
                  basePropagationValue, target, permissions, null, entryPointMap.get(method),
                  componentToIdMap);
            }
          }
        } else if (valueMap.containsKey("componentExtra")) {
          DbConnection.insertComponentExtras(entryPointMap.get(method), componentToIdMap,
              (Set<String>) valueMap.get("componentExtra"));
        }
      }
    }
  }

  private void insertDynamicReceiver(Set<String> permissions, Set<String> receiverTypes,
      BasePropagationValue intentFilters, SootMethod method, Unit unit) throws SQLException {
    if (permissions == null) {
      permissions = Collections.singleton(null);
    }

    for (String receiverType : receiverTypes) {
      for (String permission : permissions) {
        insertDynamicReceiverHelper(permission, receiverType, intentFilters, method, unit);
      }
    }
  }

  private void insertDynamicReceiverHelper(String permission, String receiverType,
      BasePropagationValue intentFilters, SootMethod method, Unit unit) throws SQLException {
    Integer missingIntentFilters;
    Set<ManifestIntentFilter> manifestIntentFilters;
    if (intentFilters == null || intentFilters instanceof TopPropagationValue
        || intentFilters instanceof BottomPropagationValue) {
      missingIntentFilters = 0;
      manifestIntentFilters = null;
    } else if (intentFilters instanceof PropagationValue) {
      missingIntentFilters = null;
      PropagationValue propagationValue = (PropagationValue) intentFilters;
      manifestIntentFilters = new HashSet<>();
      for (PathValue branchValue : propagationValue.getPathValues()) {
        Integer filterPriority = null;
        FieldValue priorityFieldValue = branchValue.getFieldValue("priority");
        if (priorityFieldValue != null) {
          filterPriority = (Integer) priorityFieldValue.getValue();
        }
        manifestIntentFilters.add(new ManifestIntentFilter(branchValue
            .getSetStringFieldValue("actions"), branchValue.getSetStringFieldValue("categories"),
            false, makeManifestData(branchValue), filterPriority));
      }
    } else {
      throw new RuntimeException("Unknown intent filter type: " + intentFilters.getClass());
    }

    ManifestComponent manifestComponent =
        new ManifestComponent(edu.psu.cse.siis.ic3.db.Constants.ComponentShortType.RECEIVER,
            receiverType, true, true, permission, null, missingIntentFilters, method, unit);
    manifestComponent.setIntentFilters(manifestIntentFilters);
    SQLConnection.insertIntentFilters(Collections.singletonList(manifestComponent));
  }

  private List<ManifestData> makeManifestData(PathValue branchValue) {
    Set<String> mimeTypes = branchValue.getSetStringFieldValue("dataType");
    Set<DataAuthority> authorities =
        branchValue.getSetFieldValue("authorities", DataAuthority.class);
    Set<String> paths = branchValue.getSetStringFieldValue("paths");
    Set<String> schemes = branchValue.getSetStringFieldValue("schemes");

    if (mimeTypes == null && authorities == null && paths == null && schemes == null) {
      return null;
    }

    if (mimeTypes == null) {
      mimeTypes = Collections.singleton(null);
    }
    if (authorities == null) {
      authorities = Collections.singleton(new DataAuthority(null, null));
    }
    if (paths == null) {
      paths = Collections.singleton(null);
    }
    if (schemes == null) {
      schemes = Collections.singleton(null);
    }

    List<ManifestData> result = new ArrayList<>();
    for (String mimeType : mimeTypes) {
      for (DataAuthority dataAuthority : authorities) {
        for (String dataPath : paths) {
          for (String scheme : schemes) {
            result.add(new ManifestData(scheme, dataAuthority.getHost(), dataAuthority.getPort(),
                dataPath, mimeType));
          }
        }
      }
    }

    return result;
  }

  private BasePropagationValue getUriValueForAuthorities(Set<String> authorities) {
    if (authorities == null) {
      return null;
    }

    PropagationValue collectingValue = new PropagationValue();
    for (String authority : authorities) {
      PathValue branchValue = new PathValue();
      ScalarFieldValue schemeFieldValue = new ScalarFieldValue("content");
      branchValue.addFieldEntry("scheme", schemeFieldValue);
      ScalarFieldValue authorityFieldValue = new ScalarFieldValue(authority);
      branchValue.addFieldEntry("authority", authorityFieldValue);
      collectingValue.addPathValue(branchValue);
    }

    return collectingValue;
  }

  private int getIdForUnit(Unit unit, SootMethod method) {
    int id = 0;
    for (Unit currentUnit : method.getActiveBody().getUnits()) {
      if (currentUnit == unit) {
        return id;
      }
      ++id;
    }

    return -1;
  }

  @SuppressWarnings("unchecked")
  private void analyzeResult(Result result) {
    Set<String> nonLinkingFieldNames = new HashSet<>();
    nonLinkingFieldNames.add("extras");
    nonLinkingFieldNames.add("flags");
    nonLinkingFieldNames.add("fragment");
    nonLinkingFieldNames.add("query");

    for (Map.Entry<Unit, Map<Integer, Object>> entry0 : result.getResults().entrySet()) {
      Collection<Object> argumentValues = entry0.getValue().values();
      boolean top = false;
      boolean bottom = false;
      // This is true only if the linking field are precisely known.
      boolean preciseLinking = true;
      // This is true only if all fields are precisely known.
      boolean preciseNonLinking = true;
      boolean nonexistent = false;
      boolean intentWithUri = false;
      boolean entryPointIntent = false;

      int resultIndex = getResultIndex((Stmt) entry0.getKey());

      for (Object value2 : argumentValues) {
        if (value2 == null) {
          nonexistent = true;
        } else if (value2 instanceof TopPropagationValue) {
          top = true;
        } else if (value2 instanceof BottomPropagationValue) {
          bottom = true;
        } else if (value2 instanceof PropagationValue) {
          // System.out.println(value2);
          Set<PathValue> pathValues = ((PropagationValue) value2).getPathValues();
          PropagationTimers.v().pathValues += pathValues.size();

          // This keeps track of all the fields that are defined across all paths.
          Set<String> definedFields = new HashSet<>();

          for (PathValue pathValue : pathValues) {
            definedFields.addAll(pathValue.getFieldMap().keySet());
          }

          Map<String, Set<FieldValue>> separateFieldValues = new HashMap<>();

          for (PathValue branchValue : pathValues) {

            intentWithUri = intentWithUri || isIntentWithUri(branchValue.getFieldMap());

            Set<String> definedFieldsInPath = new HashSet<>();

            for (Map.Entry<String, FieldValue> entry : branchValue.getFieldMap().entrySet()) {
              String fieldName = entry.getKey();
              FieldValue fieldValue = entry.getValue();

              addValueToSetMap(fieldName, fieldValue, separateFieldValues);

              definedFieldsInPath.add(fieldName);

              if (fieldValue instanceof TopFieldValue) {
                if (nonLinkingFieldNames.contains(fieldName)) {
                  preciseNonLinking = false;
                } else {
                  preciseNonLinking = false;
                  preciseLinking = false;
                }
              } else {
                Object value = fieldValue.getValue();
                if (value == null) {
                  continue;
                }

                if (value instanceof Set) {
                  Set<Object> values = (Set<Object>) value;

                  if (values.contains(Constants.ANY_STRING) || values.contains(Constants.ANY_CLASS)
                      || values.contains(Constants.ANY_INT) || values.contains(ENTRY_POINT_INTENT)
                      || values.contains("top")) {
                    if (values.contains(ENTRY_POINT_INTENT)) {
                      entryPointIntent = true;
                    }
                    preciseNonLinking = false;
                    if (!nonLinkingFieldNames.contains(fieldName)) {
                      preciseLinking = false;
                    }
                  }
                } else {
                  if (value.equals(Constants.ANY_STRING) || value.equals(Constants.ANY_CLASS)
                      || value.equals(Constants.ANY_INT) || value.equals(ENTRY_POINT_INTENT)
                      || value.equals("top")) {
                    if (value.equals(ENTRY_POINT_INTENT)) {
                      entryPointIntent = true;
                    }
                    preciseNonLinking = false;
                    if (!nonLinkingFieldNames.contains(fieldName)) {
                      preciseLinking = false;
                    }
                  }
                }
              }
            }

            // If some field is undefined in some path, then we need to keep track of a null value
            // for that field.
            Set<String> undefinedFieldForPath = new HashSet<>(definedFields);
            undefinedFieldForPath.removeAll(definedFieldsInPath);

            for (String fieldName : undefinedFieldForPath) {

              addValueToSetMap(fieldName, null, separateFieldValues);
            }
          }

          int separateCount = 1;
          for (Set<FieldValue> values : separateFieldValues.values()) {
            separateCount *= values.size();
          }
          // System.out.println(separateCount);
          PropagationTimers.v().separatePathValues += separateCount;
        }
      }

      if (intentWithUri) {
        ++this.intentWithData;
      }

      if (nonexistent) {
        if (Scene
            .v()
            .getActiveHierarchy()
            .isClassSubclassOfIncluding(
                AnalysisParameters.v().getIcfg().getMethodOf(entry0.getKey()).getDeclaringClass(),
                Scene.v().getSootClass("android.content.ContentProvider"))) {
          ++this.providerArgument;
        } else {
          ++this.nonexistent[resultIndex];
        }
      } else if (top) {
        ++this.top[resultIndex];
      } else if (bottom) {
        ++this.bottom[resultIndex];
      } else if (preciseNonLinking) {
        if (intentWithUri) {
          ++this.preciseNonLinking[3];
        } else {
          ++this.preciseNonLinking[resultIndex];
        }
      } else if (preciseLinking) {
        if (intentWithUri) {
          ++this.preciseLinking[3];
        } else {
          ++this.preciseLinking[resultIndex];
        }
      } else {
        if (entryPointIntent) {
          ++this.imprecise[4];
        } else if (intentWithUri) {
          ++this.imprecise[3];
        } else {
          ++this.imprecise[resultIndex];
        }
      }
    }
  }

  private void addValueToSetMap(String key, FieldValue value, Map<String, Set<FieldValue>> map) {
    Set<FieldValue> separateValuesForField = map.get(key);
    if (separateValuesForField == null) {
      separateValuesForField = new HashSet<>();
      map.put(key, separateValuesForField);
    }

    separateValuesForField.add(value);
  }

  private boolean isIntentWithUri(Map<String, FieldValue> fieldMap) {
    Set<String> fields = fieldMap.keySet();

    if (fields.contains("action") || fields.contains("categories")) {
      if ((fields.contains("uri") && fieldMap.get("uri") != null && fieldMap.get("uri").getValue() != null)
          || (fields.contains("path") && fieldMap.get("path") != null && fieldMap.get("path")
              .getValue() != null)
          || (fields.contains("scheme") && fieldMap.get("scheme") != null && fieldMap.get("scheme")
              .getValue() != null)
          || (fields.contains("ssp") && fieldMap.get("ssp") != null && fieldMap.get("ssp")
              .getValue() != null)) {
        return true;
      }
    }

    return false;
  }

  private int getResultIndex(Stmt stmt) {
    InvokeExpr invokeExpr = stmt.getInvokeExpr();
    List<Type> types = invokeExpr.getMethod().getParameterTypes();

    for (Type type : types) {
      if (type.toString().equals("android.content.IntentFilter")) {
        return 1;
      } else if (type.toString().equals("android.net.Uri")) {
        return 2;
      }
    }

    return 0;
  }

  private boolean containsPartialDefinition(Set<Object> values) {
    for (Object value : values) {
      if (value instanceof String && ((String) value).contains("(.*)")) {
        return true;
      }
    }

    return false;
  }
}
