/*
 * Copyright (C) 2016 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.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import java.util.HashMap;

public class Main {

  private static final String PROFILE_NAME = "primary.prof";
  private static final String APP_DIR_PREFIX = "app_dir_";
  private static final String FOREIGN_DEX_PROFILE_DIR = "foreign-dex";
  private static final String TEMP_FILE_NAME_PREFIX = "dummy";
  private static final String TEMP_FILE_NAME_SUFFIX = "-file";

  public static void main(String[] args) throws Exception {
    File tmpFile = null;
    File appDir = null;
    File profileFile = null;
    File foreignDexProfileDir = null;

    try {
      // Create the necessary files layout.
      tmpFile = createTempFile();
      appDir = new File(tmpFile.getParent(), APP_DIR_PREFIX + tmpFile.getName());
      appDir.mkdir();
      foreignDexProfileDir = new File(tmpFile.getParent(), FOREIGN_DEX_PROFILE_DIR);
      foreignDexProfileDir.mkdir();
      profileFile = createTempFile();

      String codePath = System.getenv("DEX_LOCATION") + "/577-profile-foreign-dex.jar";

      // Register the app with the runtime
      VMRuntime.registerAppInfo(profileFile.getPath(), appDir.getPath(),
             new String[] { codePath }, foreignDexProfileDir.getPath());

      testMarkerForForeignDex(foreignDexProfileDir);
      testMarkerForCodePath(foreignDexProfileDir);
      testMarkerForApplicationDexFile(foreignDexProfileDir, appDir);
    } finally {
      if (tmpFile != null) {
        tmpFile.delete();
      }
      if (profileFile != null) {
        profileFile.delete();
      }
      if (foreignDexProfileDir != null) {
        foreignDexProfileDir.delete();
      }
      if (appDir != null) {
        appDir.delete();
      }
    }
  }

  // Verify we actually create a marker on disk for foreign dex files.
  private static void testMarkerForForeignDex(File foreignDexProfileDir) throws Exception {
    String foreignDex = System.getenv("DEX_LOCATION") + "/577-profile-foreign-dex-ex.jar";
    loadDexFile(foreignDex);
    checkMarker(foreignDexProfileDir, foreignDex, /* exists */ true);
  }

  // Verify we do not create a marker on disk for dex files path of the code path.
  private static void testMarkerForCodePath(File foreignDexProfileDir) throws Exception {
    String codePath = System.getenv("DEX_LOCATION") + "/577-profile-foreign-dex.jar";
    loadDexFile(codePath);
    checkMarker(foreignDexProfileDir, codePath, /* exists */ false);
  }

  private static void testMarkerForApplicationDexFile(File foreignDexProfileDir, File appDir)
      throws Exception {
    // Copy the -ex jar to the application directory and load it from there.
    // This will record duplicate class conflicts but we don't care for this use case.
    File foreignDex = new File(System.getenv("DEX_LOCATION") + "/577-profile-foreign-dex-ex.jar");
    File appDex = new File(appDir, "appDex.jar");
    try {
      copyFile(foreignDex, appDex);

      loadDexFile(appDex.getAbsolutePath());
      checkMarker(foreignDexProfileDir, appDex.getAbsolutePath(), /* exists */ false);
    } finally {
      if (appDex != null) {
        appDex.delete();
      }
    }
  }

  private static void checkMarker(File foreignDexProfileDir, String dexFile, boolean exists) {
    File marker = new File(foreignDexProfileDir, dexFile.replace('/', '@'));
    boolean result_ok = exists ? marker.exists() : !marker.exists();
    if (!result_ok) {
      throw new RuntimeException("Marker test failed for:" + marker.getPath());
    }
  }

  private static void loadDexFile(String dexFile) throws Exception {
    Class<?> pathClassLoader = Class.forName("dalvik.system.PathClassLoader");
    if (pathClassLoader == null) {
        throw new RuntimeException("Couldn't find path class loader class");
    }
    Constructor<?> constructor =
        pathClassLoader.getDeclaredConstructor(String.class, ClassLoader.class);
    constructor.newInstance(
            dexFile, ClassLoader.getSystemClassLoader());
  }

  private static class VMRuntime {
    private static final Method registerAppInfoMethod;
    static {
      try {
        Class<?> c = Class.forName("dalvik.system.VMRuntime");
        registerAppInfoMethod = c.getDeclaredMethod("registerAppInfo",
            String.class, String.class, String[].class, String.class);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    public static void registerAppInfo(String pkgName, String appDir,
        String[] codePath, String foreignDexProfileDir) throws Exception {
      registerAppInfoMethod.invoke(null, pkgName, appDir, codePath, foreignDexProfileDir);
    }
  }

  private static void copyFile(File fromFile, File toFile) throws Exception {
    FileInputStream in = new FileInputStream(fromFile);
    FileOutputStream out = new FileOutputStream(toFile);
    try {
      byte[] buffer = new byte[4096];
      int bytesRead;
      while ((bytesRead = in.read(buffer)) >= 0) {
          out.write(buffer, 0, bytesRead);
      }
    } finally {
      out.flush();
      try {
          out.getFD().sync();
      } catch (IOException e) {
      }
      out.close();
      in.close();
    }
  }

  private static File createTempFile() throws Exception {
    try {
      return File.createTempFile(TEMP_FILE_NAME_PREFIX, TEMP_FILE_NAME_SUFFIX);
    } catch (IOException e) {
      System.setProperty("java.io.tmpdir", "/data/local/tmp");
      try {
        return File.createTempFile(TEMP_FILE_NAME_PREFIX, TEMP_FILE_NAME_SUFFIX);
      } catch (IOException e2) {
        System.setProperty("java.io.tmpdir", "/sdcard");
        return File.createTempFile(TEMP_FILE_NAME_PREFIX, TEMP_FILE_NAME_SUFFIX);
      }
    }
  }
}
