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

#define LOG_TAG "FlpHardwareProvider"
#define LOG_NDEBUG  0

#define WAKE_LOCK_NAME  "FLP"
#define LOCATION_CLASS_NAME "android/location/Location"

#include "jni.h"
#include "JNIHelp.h"
#include "android_runtime/AndroidRuntime.h"
#include "android_runtime/Log.h"
#include "hardware/fused_location.h"
#include "hardware_legacy/power.h"

static jobject sCallbacksObj = NULL;
static JNIEnv *sCallbackEnv = NULL;
static hw_device_t* sHardwareDevice = NULL;

static jmethodID sOnLocationReport = NULL;
static jmethodID sOnDataReport = NULL;
static jmethodID sOnGeofenceTransition = NULL;
static jmethodID sOnGeofenceMonitorStatus = NULL;
static jmethodID sOnGeofenceAdd = NULL;
static jmethodID sOnGeofenceRemove = NULL;
static jmethodID sOnGeofencePause = NULL;
static jmethodID sOnGeofenceResume = NULL;

static const FlpLocationInterface* sFlpInterface = NULL;
static const FlpDiagnosticInterface* sFlpDiagnosticInterface = NULL;
static const FlpGeofencingInterface* sFlpGeofencingInterface = NULL;
static const FlpDeviceContextInterface* sFlpDeviceContextInterface = NULL;

namespace android {

static inline void CheckExceptions(JNIEnv* env, const char* methodName) {
  if(!env->ExceptionCheck()) {
    return;
  }

  ALOGE("An exception was thrown by '%s'.", methodName);
  LOGE_EX(env);
  env->ExceptionClear();
}

static inline void ThrowOnError(
    JNIEnv* env,
    int resultCode,
    const char* methodName) {
  if(resultCode == FLP_RESULT_SUCCESS) {
    return;
  }

  ALOGE("Error %d in '%s'", resultCode, methodName);
  env->FatalError(methodName);
}

static bool IsValidCallbackThread() {
  JNIEnv* env = AndroidRuntime::getJNIEnv();

  if(sCallbackEnv == NULL || sCallbackEnv != env) {
    ALOGE("CallbackThread check fail: env=%p, expected=%p", env, sCallbackEnv);
    return false;
  }

  return true;
}

static int SetThreadEvent(ThreadEvent event) {
  JavaVM* javaVm = AndroidRuntime::getJavaVM();

  switch(event) {
    case ASSOCIATE_JVM:
    {
      if(sCallbackEnv != NULL) {
        ALOGE(
            "Attempted to associate callback in '%s'. Callback already associated.",
            __FUNCTION__
            );
        return FLP_RESULT_ERROR;
      }

      JavaVMAttachArgs args = {
          JNI_VERSION_1_6,
          "FLP Service Callback Thread",
          /* group */ NULL
      };

      jint attachResult = javaVm->AttachCurrentThread(&sCallbackEnv, &args);
      if (attachResult != 0) {
        ALOGE("Callback thread attachment error: %d", attachResult);
        return FLP_RESULT_ERROR;
      }

      ALOGV("Callback thread attached: %p", sCallbackEnv);
      break;
    }
    case DISASSOCIATE_JVM:
    {
      if (!IsValidCallbackThread()) {
        ALOGE(
            "Attempted to dissasociate an unnownk callback thread : '%s'.",
            __FUNCTION__
            );
        return FLP_RESULT_ERROR;
      }

      if (javaVm->DetachCurrentThread() != 0) {
        return FLP_RESULT_ERROR;
      }

      sCallbackEnv = NULL;
      break;
    }
    default:
      ALOGE("Invalid ThreadEvent request %d", event);
      return FLP_RESULT_ERROR;
  }

  return FLP_RESULT_SUCCESS;
}

/*
 * Initializes the FlpHardwareProvider class from the native side by opening
 * the HW module and obtaining the proper interfaces.
 */
static void ClassInit(JNIEnv* env, jclass clazz) {
  // get references to the Java provider methods
  sOnLocationReport = env->GetMethodID(
      clazz,
      "onLocationReport",
      "([Landroid/location/Location;)V");
  sOnDataReport = env->GetMethodID(
      clazz,
      "onDataReport",
      "(Ljava/lang/String;)V"
      );
  sOnGeofenceTransition = env->GetMethodID(
      clazz,
      "onGeofenceTransition",
      "(ILandroid/location/Location;IJI)V"
      );
  sOnGeofenceMonitorStatus = env->GetMethodID(
      clazz,
      "onGeofenceMonitorStatus",
      "(IILandroid/location/Location;)V"
      );
  sOnGeofenceAdd = env->GetMethodID(clazz, "onGeofenceAdd", "(II)V");
  sOnGeofenceRemove = env->GetMethodID(clazz, "onGeofenceRemove", "(II)V");
  sOnGeofencePause = env->GetMethodID(clazz, "onGeofencePause", "(II)V");
  sOnGeofenceResume = env->GetMethodID(clazz, "onGeofenceResume", "(II)V");
}

/*
 * Helper function to unwrap a java object back into a FlpLocation structure.
 */
static void TranslateFromObject(
    JNIEnv* env,
    jobject locationObject,
    FlpLocation& location) {
  location.size = sizeof(FlpLocation);
  location.flags = 0;

  jclass locationClass = env->GetObjectClass(locationObject);

  jmethodID getLatitude = env->GetMethodID(locationClass, "getLatitude", "()D");
  location.latitude = env->CallDoubleMethod(locationObject, getLatitude);
  jmethodID getLongitude = env->GetMethodID(locationClass, "getLongitude", "()D");
  location.longitude = env->CallDoubleMethod(locationObject, getLongitude);
  jmethodID getTime = env->GetMethodID(locationClass, "getTime", "()J");
  location.timestamp = env->CallLongMethod(locationObject, getTime);
  location.flags |= FLP_LOCATION_HAS_LAT_LONG;

  jmethodID hasAltitude = env->GetMethodID(locationClass, "hasAltitude", "()Z");
  if (env->CallBooleanMethod(locationObject, hasAltitude)) {
    jmethodID getAltitude = env->GetMethodID(locationClass, "getAltitude", "()D");
    location.altitude = env->CallDoubleMethod(locationObject, getAltitude);
    location.flags |= FLP_LOCATION_HAS_ALTITUDE;
  }

  jmethodID hasSpeed = env->GetMethodID(locationClass, "hasSpeed", "()Z");
  if (env->CallBooleanMethod(locationObject, hasSpeed)) {
    jmethodID getSpeed = env->GetMethodID(locationClass, "getSpeed", "()F");
    location.speed = env->CallFloatMethod(locationObject, getSpeed);
    location.flags |= FLP_LOCATION_HAS_SPEED;
  }

  jmethodID hasBearing = env->GetMethodID(locationClass, "hasBearing", "()Z");
  if (env->CallBooleanMethod(locationObject, hasBearing)) {
    jmethodID getBearing = env->GetMethodID(locationClass, "getBearing", "()F");
    location.bearing = env->CallFloatMethod(locationObject, getBearing);
    location.flags |= FLP_LOCATION_HAS_BEARING;
  }

  jmethodID hasAccuracy = env->GetMethodID(locationClass, "hasAccuracy", "()Z");
  if (env->CallBooleanMethod(locationObject, hasAccuracy)) {
    jmethodID getAccuracy = env->GetMethodID(
        locationClass,
        "getAccuracy",
        "()F"
        );
    location.accuracy = env->CallFloatMethod(locationObject, getAccuracy);
    location.flags |= FLP_LOCATION_HAS_ACCURACY;
  }

  // TODO: wire sources_used if Location class exposes them

  env->DeleteLocalRef(locationClass);
}

/*
 * Helper function to unwrap FlpBatchOptions from the Java Runtime calls.
 */
static void TranslateFromObject(
    JNIEnv* env,
    jobject batchOptionsObject,
    FlpBatchOptions& batchOptions) {
  jclass batchOptionsClass = env->GetObjectClass(batchOptionsObject);

  jmethodID getMaxPower = env->GetMethodID(
      batchOptionsClass,
      "getMaxPowerAllocationInMW",
      "()D"
      );
  batchOptions.max_power_allocation_mW = env->CallDoubleMethod(
      batchOptionsObject,
      getMaxPower
      );

  jmethodID getPeriod = env->GetMethodID(
      batchOptionsClass,
      "getPeriodInNS",
      "()J"
      );
  batchOptions.period_ns = env->CallLongMethod(batchOptionsObject, getPeriod);

  jmethodID getSourcesToUse = env->GetMethodID(
      batchOptionsClass,
      "getSourcesToUse",
      "()I"
      );
  batchOptions.sources_to_use = env->CallIntMethod(
      batchOptionsObject,
      getSourcesToUse
      );

  jmethodID getFlags = env->GetMethodID(batchOptionsClass, "getFlags", "()I");
  batchOptions.flags = env->CallIntMethod(batchOptionsObject, getFlags);

  env->DeleteLocalRef(batchOptionsClass);
}

/*
 * Helper function to unwrap Geofence structures from the Java Runtime calls.
 */
static void TranslateGeofenceFromGeofenceHardwareRequestParcelable(
    JNIEnv* env,
    jobject geofenceRequestObject,
    Geofence& geofence) {
  jclass geofenceRequestClass = env->GetObjectClass(geofenceRequestObject);

  jmethodID getId = env->GetMethodID(geofenceRequestClass, "getId", "()I");
  geofence.geofence_id = env->CallIntMethod(geofenceRequestObject, getId);

  jmethodID getType = env->GetMethodID(geofenceRequestClass, "getType", "()I");
  // this works because GeofenceHardwareRequest.java and fused_location.h have
  // the same notion of geofence types
  GeofenceType type = (GeofenceType)env->CallIntMethod(geofenceRequestObject, getType);
  if(type != TYPE_CIRCLE) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }
  geofence.data->type = type;
  GeofenceCircle& circle = geofence.data->geofence.circle;

  jmethodID getLatitude = env->GetMethodID(
      geofenceRequestClass,
      "getLatitude",
      "()D");
  circle.latitude = env->CallDoubleMethod(geofenceRequestObject, getLatitude);

  jmethodID getLongitude = env->GetMethodID(
      geofenceRequestClass,
      "getLongitude",
      "()D");
  circle.longitude = env->CallDoubleMethod(geofenceRequestObject, getLongitude);

  jmethodID getRadius = env->GetMethodID(geofenceRequestClass, "getRadius", "()D");
  circle.radius_m = env->CallDoubleMethod(geofenceRequestObject, getRadius);

  GeofenceOptions* options = geofence.options;
  jmethodID getMonitorTransitions = env->GetMethodID(
      geofenceRequestClass,
      "getMonitorTransitions",
      "()I");
  options->monitor_transitions = env->CallIntMethod(
      geofenceRequestObject,
      getMonitorTransitions);

  jmethodID getUnknownTimer = env->GetMethodID(
      geofenceRequestClass,
      "getUnknownTimer",
      "()I");
  options->unknown_timer_ms = env->CallIntMethod(geofenceRequestObject, getUnknownTimer);

  jmethodID getNotificationResponsiveness = env->GetMethodID(
      geofenceRequestClass,
      "getNotificationResponsiveness",
      "()I");
  options->notification_responsivenes_ms = env->CallIntMethod(
      geofenceRequestObject,
      getNotificationResponsiveness);

  jmethodID getLastTransition = env->GetMethodID(
      geofenceRequestClass,
      "getLastTransition",
      "()I");
  options->last_transition = env->CallIntMethod(geofenceRequestObject, getLastTransition);

  // TODO: set data.sources_to_use when available

  env->DeleteLocalRef(geofenceRequestClass);
}

/*
 * Helper function to transform FlpLocation into a java object.
 */
static void TranslateToObject(const FlpLocation* location, jobject& locationObject) {
  jclass locationClass = sCallbackEnv->FindClass(LOCATION_CLASS_NAME);
  jmethodID locationCtor = sCallbackEnv->GetMethodID(
      locationClass,
      "<init>",
      "(Ljava/lang/String;)V"
      );

  // the provider is set in the upper JVM layer
  locationObject = sCallbackEnv->NewObject(locationClass, locationCtor, NULL);
  jint flags = location->flags;

  // set the valid information in the object
  if (flags & FLP_LOCATION_HAS_LAT_LONG) {
    jmethodID setLatitude = sCallbackEnv->GetMethodID(
        locationClass,
        "setLatitude",
        "(D)V"
        );
    sCallbackEnv->CallVoidMethod(locationObject, setLatitude, location->latitude);

    jmethodID setLongitude = sCallbackEnv->GetMethodID(
        locationClass,
        "setLongitude",
        "(D)V"
        );
    sCallbackEnv->CallVoidMethod(
        locationObject,
        setLongitude,
        location->longitude
        );

    jmethodID setTime = sCallbackEnv->GetMethodID(
        locationClass,
        "setTime",
        "(J)V"
        );
    sCallbackEnv->CallVoidMethod(locationObject, setTime, location->timestamp);
  }

  if (flags & FLP_LOCATION_HAS_ALTITUDE) {
    jmethodID setAltitude = sCallbackEnv->GetMethodID(
        locationClass,
        "setAltitude",
        "(D)V"
        );
    sCallbackEnv->CallVoidMethod(locationObject, setAltitude, location->altitude);
  }

  if (flags & FLP_LOCATION_HAS_SPEED) {
    jmethodID setSpeed = sCallbackEnv->GetMethodID(
        locationClass,
        "setSpeed",
        "(F)V"
        );
    sCallbackEnv->CallVoidMethod(locationObject, setSpeed, location->speed);
  }

  if (flags & FLP_LOCATION_HAS_BEARING) {
    jmethodID setBearing = sCallbackEnv->GetMethodID(
        locationClass,
        "setBearing",
        "(F)V"
        );
    sCallbackEnv->CallVoidMethod(locationObject, setBearing, location->bearing);
  }

  if (flags & FLP_LOCATION_HAS_ACCURACY) {
    jmethodID setAccuracy = sCallbackEnv->GetMethodID(
        locationClass,
        "setAccuracy",
        "(F)V"
        );
    sCallbackEnv->CallVoidMethod(locationObject, setAccuracy, location->accuracy);
  }

  // TODO: wire FlpLocation::sources_used when needed

  sCallbackEnv->DeleteLocalRef(locationClass);
}

/*
 * Helper function to serialize FlpLocation structures.
 */
static void TranslateToObjectArray(
    int32_t locationsCount,
    FlpLocation** locations,
    jobjectArray& locationsArray) {
  jclass locationClass = sCallbackEnv->FindClass(LOCATION_CLASS_NAME);
  locationsArray = sCallbackEnv->NewObjectArray(
      locationsCount,
      locationClass,
      /* initialElement */ NULL
      );

  for (int i = 0; i < locationsCount; ++i) {
    jobject locationObject = NULL;
    TranslateToObject(locations[i], locationObject);
    sCallbackEnv->SetObjectArrayElement(locationsArray, i, locationObject);
    sCallbackEnv->DeleteLocalRef(locationObject);
  }

  sCallbackEnv->DeleteLocalRef(locationClass);
}

static void LocationCallback(int32_t locationsCount, FlpLocation** locations) {
  if(!IsValidCallbackThread()) {
    return;
  }

  if(locationsCount == 0 || locations == NULL) {
    ALOGE(
        "Invalid LocationCallback. Count: %d, Locations: %p",
        locationsCount,
        locations
        );
    return;
  }

  jobjectArray locationsArray = NULL;
  TranslateToObjectArray(locationsCount, locations, locationsArray);

  sCallbackEnv->CallVoidMethod(
      sCallbacksObj,
      sOnLocationReport,
      locationsArray
      );
  CheckExceptions(sCallbackEnv, __FUNCTION__);

  if(locationsArray != NULL) {
    sCallbackEnv->DeleteLocalRef(locationsArray);
  }
}

static void AcquireWakelock() {
  acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_NAME);
}

static void ReleaseWakelock() {
  release_wake_lock(WAKE_LOCK_NAME);
}

FlpCallbacks sFlpCallbacks = {
  sizeof(FlpCallbacks),
  LocationCallback,
  AcquireWakelock,
  ReleaseWakelock,
  SetThreadEvent
};

static void ReportData(char* data, int length) {
  jstring stringData = NULL;

  if(length != 0 && data != NULL) {
    stringData = sCallbackEnv->NewString(reinterpret_cast<jchar*>(data), length);
  } else {
    ALOGE("Invalid ReportData callback. Length: %d, Data: %p", length, data);
    return;
  }

  sCallbackEnv->CallVoidMethod(sCallbacksObj, sOnDataReport, stringData);
  CheckExceptions(sCallbackEnv, __FUNCTION__);
}

FlpDiagnosticCallbacks sFlpDiagnosticCallbacks = {
  sizeof(FlpDiagnosticCallbacks),
  SetThreadEvent,
  ReportData
};

static void GeofenceTransitionCallback(
    int32_t geofenceId,
    FlpLocation* location,
    int32_t transition,
    FlpUtcTime timestamp,
    uint32_t sourcesUsed
    ) {
  if(!IsValidCallbackThread()) {
    return;
  }

  if(location == NULL) {
    ALOGE("GeofenceTransition received with invalid location: %p", location);
    return;
  }

  jobject locationObject = NULL;
  TranslateToObject(location, locationObject);

  sCallbackEnv->CallVoidMethod(
      sCallbacksObj,
      sOnGeofenceTransition,
      geofenceId,
      locationObject,
      transition,
      timestamp,
      sourcesUsed
      );
  CheckExceptions(sCallbackEnv, __FUNCTION__);

  if(locationObject != NULL) {
    sCallbackEnv->DeleteLocalRef(locationObject);
  }
}

static void GeofenceMonitorStatusCallback(
    int32_t status,
    uint32_t source,
    FlpLocation* lastLocation) {
  if(!IsValidCallbackThread()) {
    return;
  }

  jobject locationObject = NULL;
  if(lastLocation != NULL) {
    TranslateToObject(lastLocation, locationObject);
  }

  sCallbackEnv->CallVoidMethod(
      sCallbacksObj,
      sOnGeofenceMonitorStatus,
      status,
      source,
      locationObject
      );
  CheckExceptions(sCallbackEnv, __FUNCTION__);

  if(locationObject != NULL) {
    sCallbackEnv->DeleteLocalRef(locationObject);
  }
}

static void GeofenceAddCallback(int32_t geofenceId, int32_t result) {
  if(!IsValidCallbackThread()) {
    return;
  }

  sCallbackEnv->CallVoidMethod(sCallbacksObj, sOnGeofenceAdd, geofenceId, result);
  CheckExceptions(sCallbackEnv, __FUNCTION__);
}

static void GeofenceRemoveCallback(int32_t geofenceId, int32_t result) {
  if(!IsValidCallbackThread()) {
    return;
  }

  sCallbackEnv->CallVoidMethod(
      sCallbacksObj,
      sOnGeofenceRemove,
      geofenceId,
      result
      );
  CheckExceptions(sCallbackEnv, __FUNCTION__);
}

static void GeofencePauseCallback(int32_t geofenceId, int32_t result) {
  if(!IsValidCallbackThread()) {
    return;
  }

  sCallbackEnv->CallVoidMethod(
      sCallbacksObj,
      sOnGeofencePause,
      geofenceId,
      result
      );
  CheckExceptions(sCallbackEnv, __FUNCTION__);
}

static void GeofenceResumeCallback(int32_t geofenceId, int32_t result) {
  if(!IsValidCallbackThread()) {
    return;
  }

  sCallbackEnv->CallVoidMethod(
      sCallbacksObj,
      sOnGeofenceResume,
      geofenceId,
      result
      );
  CheckExceptions(sCallbackEnv, __FUNCTION__);
}

FlpGeofenceCallbacks sFlpGeofenceCallbacks = {
  sizeof(FlpGeofenceCallbacks),
  GeofenceTransitionCallback,
  GeofenceMonitorStatusCallback,
  GeofenceAddCallback,
  GeofenceRemoveCallback,
  GeofencePauseCallback,
  GeofenceResumeCallback,
  SetThreadEvent
};

/*
 * Initializes the Fused Location Provider in the native side. It ensures that
 * the Flp interfaces are initialized properly.
 */
static void Init(JNIEnv* env, jobject obj) {
  if(sHardwareDevice != NULL) {
    ALOGD("Hardware Device already opened.");
    return;
  }

  const hw_module_t* module = NULL;
  int err = hw_get_module(FUSED_LOCATION_HARDWARE_MODULE_ID, &module);
  if(err != 0) {
    ALOGE("Error hw_get_module '%s': %d", FUSED_LOCATION_HARDWARE_MODULE_ID, err);
    return;
  }

  err = module->methods->open(
        module,
        FUSED_LOCATION_HARDWARE_MODULE_ID, &sHardwareDevice);
  if(err != 0) {
    ALOGE("Error opening device '%s': %d", FUSED_LOCATION_HARDWARE_MODULE_ID, err);
    return;
  }

  sFlpInterface = NULL;
  flp_device_t* flp_device = reinterpret_cast<flp_device_t*>(sHardwareDevice);
  sFlpInterface = flp_device->get_flp_interface(flp_device);

  if(sFlpInterface != NULL) {
    sFlpDiagnosticInterface = reinterpret_cast<const FlpDiagnosticInterface*>(
        sFlpInterface->get_extension(FLP_DIAGNOSTIC_INTERFACE)
        );

    sFlpGeofencingInterface = reinterpret_cast<const FlpGeofencingInterface*>(
        sFlpInterface->get_extension(FLP_GEOFENCING_INTERFACE)
        );

    sFlpDeviceContextInterface = reinterpret_cast<const FlpDeviceContextInterface*>(
        sFlpInterface->get_extension(FLP_DEVICE_CONTEXT_INTERFACE)
        );
  }

  if(sCallbacksObj == NULL) {
    sCallbacksObj = env->NewGlobalRef(obj);
  }

  // initialize the Flp interfaces
  if(sFlpInterface == NULL || sFlpInterface->init(&sFlpCallbacks) != 0) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  if(sFlpDiagnosticInterface != NULL) {
    sFlpDiagnosticInterface->init(&sFlpDiagnosticCallbacks);
  }

  if(sFlpGeofencingInterface != NULL) {
    sFlpGeofencingInterface->init(&sFlpGeofenceCallbacks);
  }

  // TODO: inject any device context if when needed
}

static jboolean IsSupported(JNIEnv* env, jclass clazz) {
  return sFlpInterface != NULL;
}

static jint GetBatchSize(JNIEnv* env, jobject object) {
  if(sFlpInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  return sFlpInterface->get_batch_size();
}

static void StartBatching(
    JNIEnv* env,
    jobject object,
    jint id,
    jobject optionsObject) {
  if(sFlpInterface == NULL || optionsObject == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  FlpBatchOptions options;
  TranslateFromObject(env, optionsObject, options);
  int result = sFlpInterface->start_batching(id, &options);
  ThrowOnError(env, result, __FUNCTION__);
}

static void UpdateBatchingOptions(
    JNIEnv* env,
    jobject object,
    jint id,
    jobject optionsObject) {
  if(sFlpInterface == NULL || optionsObject == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  FlpBatchOptions options;
  TranslateFromObject(env, optionsObject, options);
  int result = sFlpInterface->update_batching_options(id, &options);
  ThrowOnError(env, result, __FUNCTION__);
}

static void StopBatching(JNIEnv* env, jobject object, jint id) {
  if(sFlpInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  sFlpInterface->stop_batching(id);
}

static void Cleanup(JNIEnv* env, jobject object) {
  if(sFlpInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  sFlpInterface->cleanup();

  if(sCallbacksObj != NULL) {
    env->DeleteGlobalRef(sCallbacksObj);
    sCallbacksObj = NULL;
  }

  sFlpInterface = NULL;
  sFlpDiagnosticInterface = NULL;
  sFlpDeviceContextInterface = NULL;
  sFlpGeofencingInterface = NULL;

  if(sHardwareDevice != NULL) {
    sHardwareDevice->close(sHardwareDevice);
    sHardwareDevice = NULL;
  }
}

static void GetBatchedLocation(JNIEnv* env, jobject object, jint lastNLocations) {
  if(sFlpInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  sFlpInterface->get_batched_location(lastNLocations);
}

static void InjectLocation(JNIEnv* env, jobject object, jobject locationObject) {
  if(locationObject == NULL) {
    ALOGE("Invalid location for injection: %p", locationObject);
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  if(sFlpInterface == NULL) {
    // there is no listener, bail
    return;
  }

  FlpLocation location;
  TranslateFromObject(env, locationObject, location);
  int result = sFlpInterface->inject_location(&location);
  if (result != FLP_RESULT_SUCCESS) {
    // do not throw but log, this operation should be fire and forget
    ALOGE("Error %d in '%s'", result, __FUNCTION__);
  }
}

static jboolean IsDiagnosticSupported() {
  return sFlpDiagnosticInterface != NULL;
}

static void InjectDiagnosticData(JNIEnv* env, jobject object, jstring stringData) {
  if(stringData == NULL) {
    ALOGE("Invalid diagnostic data for injection: %p", stringData);
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  if(sFlpDiagnosticInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  int length = env->GetStringLength(stringData);
  const jchar* data = env->GetStringChars(stringData, /* isCopy */ NULL);
  if(data == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  int result = sFlpDiagnosticInterface->inject_data((char*) data, length);
  ThrowOnError(env, result, __FUNCTION__);
}

static jboolean IsDeviceContextSupported() {
  return sFlpDeviceContextInterface != NULL;
}

static void InjectDeviceContext(JNIEnv* env, jobject object, jint enabledMask) {
  if(sFlpDeviceContextInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  int result = sFlpDeviceContextInterface->inject_device_context(enabledMask);
  ThrowOnError(env, result, __FUNCTION__);
}

static jboolean IsGeofencingSupported() {
  return sFlpGeofencingInterface != NULL;
}

static void AddGeofences(
    JNIEnv* env,
    jobject object,
    jobjectArray geofenceRequestsArray) {
  if(geofenceRequestsArray == NULL) {
    ALOGE("Invalid Geofences to add: %p", geofenceRequestsArray);
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  if (sFlpGeofencingInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  jint geofenceRequestsCount = env->GetArrayLength(geofenceRequestsArray);
  if(geofenceRequestsCount == 0) {
    return;
  }

  Geofence* geofences = new Geofence[geofenceRequestsCount];
  if (geofences == NULL) {
    ThrowOnError(env, FLP_RESULT_INSUFFICIENT_MEMORY, __FUNCTION__);
  }

  for (int i = 0; i < geofenceRequestsCount; ++i) {
    geofences[i].data = new GeofenceData();
    geofences[i].options = new GeofenceOptions();
    jobject geofenceObject = env->GetObjectArrayElement(geofenceRequestsArray, i);

    TranslateGeofenceFromGeofenceHardwareRequestParcelable(env, geofenceObject, geofences[i]);
    env->DeleteLocalRef(geofenceObject);
  }

  sFlpGeofencingInterface->add_geofences(geofenceRequestsCount, &geofences);
  if (geofences != NULL) {
    for(int i = 0; i < geofenceRequestsCount; ++i) {
      delete geofences[i].data;
      delete geofences[i].options;
    }
    delete[] geofences;
  }
}

static void PauseGeofence(JNIEnv* env, jobject object, jint geofenceId) {
  if(sFlpGeofencingInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  sFlpGeofencingInterface->pause_geofence(geofenceId);
}

static void ResumeGeofence(
    JNIEnv* env,
    jobject object,
    jint geofenceId,
    jint monitorTransitions) {
  if(sFlpGeofencingInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  sFlpGeofencingInterface->resume_geofence(geofenceId, monitorTransitions);
}

static void ModifyGeofenceOption(
    JNIEnv* env,
    jobject object,
    jint geofenceId,
    jint lastTransition,
    jint monitorTransitions,
    jint notificationResponsiveness,
    jint unknownTimer,
    jint sourcesToUse) {
  if(sFlpGeofencingInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  GeofenceOptions options = {
      lastTransition,
      monitorTransitions,
      notificationResponsiveness,
      unknownTimer,
      (uint32_t)sourcesToUse
  };

  sFlpGeofencingInterface->modify_geofence_option(geofenceId, &options);
}

static void RemoveGeofences(
    JNIEnv* env,
    jobject object,
    jintArray geofenceIdsArray) {
  if(sFlpGeofencingInterface == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  jsize geofenceIdsCount = env->GetArrayLength(geofenceIdsArray);
  jint* geofenceIds = env->GetIntArrayElements(geofenceIdsArray, /* isCopy */ NULL);
  if(geofenceIds == NULL) {
    ThrowOnError(env, FLP_RESULT_ERROR, __FUNCTION__);
  }

  sFlpGeofencingInterface->remove_geofences(geofenceIdsCount, geofenceIds);
  env->ReleaseIntArrayElements(geofenceIdsArray, geofenceIds, 0 /*mode*/);
}

static JNINativeMethod sMethods[] = {
  //{"name", "signature", functionPointer }
  {"nativeClassInit", "()V", reinterpret_cast<void*>(ClassInit)},
  {"nativeInit", "()V", reinterpret_cast<void*>(Init)},
  {"nativeCleanup", "()V", reinterpret_cast<void*>(Cleanup)},
  {"nativeIsSupported", "()Z", reinterpret_cast<void*>(IsSupported)},
  {"nativeGetBatchSize", "()I", reinterpret_cast<void*>(GetBatchSize)},
  {"nativeStartBatching",
        "(ILandroid/location/FusedBatchOptions;)V",
        reinterpret_cast<void*>(StartBatching)},
  {"nativeUpdateBatchingOptions",
        "(ILandroid/location/FusedBatchOptions;)V",
        reinterpret_cast<void*>(UpdateBatchingOptions)},
  {"nativeStopBatching", "(I)V", reinterpret_cast<void*>(StopBatching)},
  {"nativeRequestBatchedLocation",
        "(I)V",
        reinterpret_cast<void*>(GetBatchedLocation)},
  {"nativeInjectLocation",
        "(Landroid/location/Location;)V",
        reinterpret_cast<void*>(InjectLocation)},
  {"nativeIsDiagnosticSupported",
        "()Z",
        reinterpret_cast<void*>(IsDiagnosticSupported)},
  {"nativeInjectDiagnosticData",
        "(Ljava/lang/String;)V",
        reinterpret_cast<void*>(InjectDiagnosticData)},
  {"nativeIsDeviceContextSupported",
        "()Z",
        reinterpret_cast<void*>(IsDeviceContextSupported)},
  {"nativeInjectDeviceContext",
        "(I)V",
        reinterpret_cast<void*>(InjectDeviceContext)},
  {"nativeIsGeofencingSupported",
        "()Z",
        reinterpret_cast<void*>(IsGeofencingSupported)},
  {"nativeAddGeofences",
        "([Landroid/hardware/location/GeofenceHardwareRequestParcelable;)V",
        reinterpret_cast<void*>(AddGeofences)},
  {"nativePauseGeofence", "(I)V", reinterpret_cast<void*>(PauseGeofence)},
  {"nativeResumeGeofence", "(II)V", reinterpret_cast<void*>(ResumeGeofence)},
  {"nativeModifyGeofenceOption",
        "(IIIIII)V",
        reinterpret_cast<void*>(ModifyGeofenceOption)},
  {"nativeRemoveGeofences", "([I)V", reinterpret_cast<void*>(RemoveGeofences)}
};

/*
 * Registration method invoked on JNI Load.
 */
int register_android_server_location_FlpHardwareProvider(JNIEnv* env) {
  return jniRegisterNativeMethods(
      env,
      "com/android/server/location/FlpHardwareProvider",
      sMethods,
      NELEM(sMethods)
      );
}

} /* name-space Android */