/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2011-2017 - Daniel De Matteis
 *  Copyright (C) 2014-2017 - Jean-André Santoni
 *  Copyright (C) 2016-2019 - Brad Parker
 *  Copyright (C) 2019-2020 - James Leaver
 *
 *  RetroArch is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with RetroArch.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include "com_retroarch_browser_retroactivity_RetroActivityCommon.h"

#ifdef HAVE_THREADS
#include <rthreads/rthreads.h>
#include <stdlib.h>
#endif

#include <string/stdstring.h>
#include "../frontend/drivers/platform_unix.h"

#include "play_feature_delivery.h"

/***************************/
/* Globals (do not fix...) */
/***************************/

/* Due to the way the JNI interface works,
 * core download status updates happen
 * asynchronously in a manner that we cannot
 * capture using any standard means. We therefore
 * have to implement status monitoring via an
 * ugly hack, involving a mutex-locked global
 * status struct... */

typedef struct
{
#ifdef HAVE_THREADS
   slock_t *enabled_lock;
   slock_t *status_lock;
#endif
   unsigned download_progress;
   enum play_feature_delivery_install_status last_status;
   char last_core_name[256];
   bool enabled;
   bool enabled_set;
   bool active;
} play_feature_delivery_state_t;

static play_feature_delivery_state_t play_feature_delivery_state = {

#ifdef HAVE_THREADS
   NULL,                       /* enabled_lock */
   NULL,                       /* status_lock */
#endif
   0,                          /* download_progress */
   PLAY_FEATURE_DELIVERY_IDLE, /* last_status */
   {'\0'},                     /* last_core_name */
   false,                      /* enabled */
   false,                      /* enabled_set */
   false,                      /* active */
};

static play_feature_delivery_state_t* play_feature_delivery_get_state(void)
{
   return &play_feature_delivery_state;
}

/**********************/
/* JNI Native Methods */
/**********************/

/*
 * Class:     com_retroarch_browser_retroactivity_RetroActivityCommon
 * Method:    coreInstallInitiated
 * Signature: (Ljava/lang/String;Z)V
 */
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_RetroActivityCommon_coreInstallInitiated
      (JNIEnv *env, jobject this_obj, jstring core_name, jboolean successful)
{
   play_feature_delivery_state_t* state = play_feature_delivery_get_state();

   /* Lock mutex */
#ifdef HAVE_THREADS
   slock_lock(state->status_lock);
#endif

   /* Only update status if an install is active */
   if (state->active)
   {
      /* Convert Java-style string to a proper char array */
      const char *core_name_c = (*env)->GetStringUTFChars(
            env, core_name, NULL);

      /* Ensure that status update is for the
       * correct core */
      if (string_is_equal(state->last_core_name, core_name_c))
      {
         if (successful)
            state->last_status = PLAY_FEATURE_DELIVERY_STARTING;
         else
         {
            state->last_status = PLAY_FEATURE_DELIVERY_FAILED;
            state->active      = false;
         }
      }

      /* Must always 'release' the converted string */
      (*env)->ReleaseStringUTFChars(env, core_name, core_name_c);
   }

   /* Unlock mutex */
#ifdef HAVE_THREADS
   slock_unlock(state->status_lock);
#endif
}

/*
 * Class:     com_retroarch_browser_retroactivity_RetroActivityCommon
 * Method:    coreInstallStatusChanged
 * Signature: ([Ljava/lang/String;IJJ)V
 */
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_RetroActivityCommon_coreInstallStatusChanged
      (JNIEnv *env, jobject thisObj, jobjectArray core_names, jint status, jlong bytes_downloaded, jlong total_bytes_to_download)
{
   play_feature_delivery_state_t* state = play_feature_delivery_get_state();

   /* Lock mutex */
#ifdef HAVE_THREADS
   slock_lock(state->status_lock);
#endif

   /* Only update status if an install is active */
   if (state->active)
   {
      /* Note: core_names is a list of cores that
       * are currently installing. We should check
       * that state->last_core_name is in this list
       * before updating the status, but if multiple
       * installs are queued then it seems dubious
       * to filter like this - i.e. is it possible
       * for an entry to drop off the queue before
       * the entire transaction is complete? If so,
       * then we may risk 'missing' the final status
       * update...
       * We therefore just monitor the transaction
       * as a whole, and disregard core names... */

      /* Determine download progress */
      if (total_bytes_to_download > 0)
      {
         state->download_progress = (unsigned)
               (((float)bytes_downloaded * 100.0f /
                     (float)total_bytes_to_download) + 0.5f);
         state->download_progress = (state->download_progress > 100) ?
               100 : state->download_progress;
      }
      else
         state->download_progress = 100;

      /* Check status */
      switch (status)
      {
         case 0: /* INSTALL_STATUS_DOWNLOADING */
            state->last_status = PLAY_FEATURE_DELIVERY_DOWNLOADING;
            break;
         case 1: /* INSTALL_STATUS_INSTALLING */
            state->last_status = PLAY_FEATURE_DELIVERY_INSTALLING;
            break;
         case 2: /* INSTALL_STATUS_INSTALLED */
            state->last_status = PLAY_FEATURE_DELIVERY_INSTALLED;
            state->active      = false;
            break;
         case 3: /* INSTALL_STATUS_FAILED */
         default:
            state->last_status = PLAY_FEATURE_DELIVERY_FAILED;
            state->active      = false;
            break;
      }
   }

   /* Unlock mutex */
#ifdef HAVE_THREADS
   slock_unlock(state->status_lock);
#endif
}

/******************/
/* Initialisation */
/******************/

/* Must be called upon program initialisation */
void play_feature_delivery_init(void)
{
   play_feature_delivery_state_t* state = play_feature_delivery_get_state();

   play_feature_delivery_deinit();

#ifdef HAVE_THREADS
   if (!state->enabled_lock)
      state->enabled_lock = slock_new();
   if (!state->status_lock)
      state->status_lock  = slock_new();
#endif

   /* Note: Would like to cache whether this
    * is a Play Store build here, but
    * play_feature_delivery_init() is called
    * too early in the startup sequence... */
}

/* Must be called upon program termination */
void play_feature_delivery_deinit(void)
{
   play_feature_delivery_state_t* state = play_feature_delivery_get_state();

#ifdef HAVE_THREADS
   if (state->enabled_lock)
   {
      slock_free(state->enabled_lock);
      state->enabled_lock = NULL;
   }

   if (state->status_lock)
   {
      slock_free(state->status_lock);
      state->status_lock = NULL;
   }
#endif
}

/**********/
/* Status */
/**********/

static bool play_feature_delivery_get_core_name(
      const char *core_file, char *core_name, size_t len)
{
   size_t core_file_len;

   if (string_is_empty(core_file))
      return false;

   core_file_len = strlen(core_file);

   if (len < core_file_len)
      return false;

   /* Ensure that core_file has the correct
    * suffix */
   if (!string_ends_with_size(core_file, "_libretro_android.so",
         core_file_len, STRLEN_CONST("_libretro_android.so")))
      return false;

   /* Copy core_file and remove suffix */
   strlcpy(core_name, core_file, len);
   core_name[core_file_len - STRLEN_CONST("_libretro_android.so")] = '\0';

   return true;
}

/* Returns true if current build utilises
 * play feature delivery
 * > Relies on a Java function call, and is
 *   therefore slow */
static bool play_feature_delivery_enabled_internal(void)
{
   JNIEnv *env             = jni_thread_getenv();
   struct android_app *app = (struct android_app*)g_android;
   bool enabled            = false;

   if (!env ||
       !app ||
       !app->isPlayStoreBuild)
      return false;

   CALL_BOOLEAN_METHOD(env, enabled, app->activity->clazz,
         app->isPlayStoreBuild);

   return enabled;
}

/* Returns true if current build utilises
 * play feature delivery */
bool play_feature_delivery_enabled(void)
{
   play_feature_delivery_state_t* state = play_feature_delivery_get_state();
   bool enabled;

   /* Lock mutex */
#ifdef HAVE_THREADS
   slock_lock(state->enabled_lock);
#endif

   /* Calling Java functions is slow. We need to
    * check Play Store build status frequently,
    * often in loops, so rely on a cached global
    * status flag instead dealing with Java
    * interfaces */
   if (!state->enabled_set)
   {
      state->enabled     = play_feature_delivery_enabled_internal();
      state->enabled_set = true;
   }

   enabled = state->enabled;

   /* Unlock mutex */
#ifdef HAVE_THREADS
   slock_unlock(state->enabled_lock);
#endif

   return enabled;
}

/* Returns a list of cores currently available
 * via play feature delivery.
 * Returns a new string_list on success, or
 * NULL on failure */
struct string_list *play_feature_delivery_available_cores(void)
{
   JNIEnv *env                   = jni_thread_getenv();
   struct android_app *app       = (struct android_app*)g_android;
   struct string_list *core_list = string_list_new();
   union string_list_elem_attr attr;
   jobjectArray available_cores;
   jsize num_cores;
   size_t i;

   attr.i = 0;

   if (!env ||
       !app ||
       !app->getAvailableCores ||
       !core_list)
      goto error;

   /* Get list of available cores */
   CALL_OBJ_METHOD(env, available_cores, app->activity->clazz,
         app->getAvailableCores);
   num_cores = (*env)->GetArrayLength(env, available_cores);

   for (i = 0; i < num_cores; i++)
   {
      /* Extract element of available cores array */
      jstring core_name_jni = (jstring)
            ((*env)->GetObjectArrayElement(env, available_cores, i));
      const char *core_name = NULL;

      /* Convert Java-style string to a proper char array */
      core_name = (*env)->GetStringUTFChars(env, core_name_jni, NULL);

      if (!string_is_empty(core_name))
      {
         char core_file[256];
         /* Generate core file name */
         size_t _len = strlcpy(core_file, core_name, sizeof(core_file));
         strlcpy(core_file       + _len,
               "_libretro_android.so",
               sizeof(core_file) - _len);
         /* Add entry to list */
         if (!string_is_empty(core_file))
            string_list_append(core_list, core_file, attr);
      }

      /* Must always 'release' the converted string */
      (*env)->ReleaseStringUTFChars(env, core_name_jni, core_name);
   }

   if (core_list->size < 1)
      goto error;

   return core_list;

error:
   if (core_list)
      string_list_free(core_list);

   return NULL;
}

/* Returns true if specified core is currently
 * installed via play feature delivery */
bool play_feature_delivery_core_installed(const char *core_file)
{
   JNIEnv *env             = jni_thread_getenv();
   struct android_app *app = (struct android_app*)g_android;
   jobjectArray installed_cores;
   jsize num_cores;
   char core_name[256];
   size_t i;

   core_name[0] = '\0';

   if (!env ||
       !app ||
       !app->getInstalledCores)
      return false;

   /* Extract core name */
   if (!play_feature_delivery_get_core_name(
         core_file, core_name, sizeof(core_name)))
      return false;

   /* Get list of installed cores */
   CALL_OBJ_METHOD(env, installed_cores, app->activity->clazz,
         app->getInstalledCores);
   num_cores = (*env)->GetArrayLength(env, installed_cores);

   for (i = 0; i < num_cores; i++)
   {
      /* Extract element of installed cores array */
      jstring installed_core_name_jni = (jstring)
            ((*env)->GetObjectArrayElement(env, installed_cores, i));
      const char *installed_core_name = NULL;

      /* Convert Java-style string to a proper char array */
      installed_core_name = (*env)->GetStringUTFChars(
            env, installed_core_name_jni, NULL);

      /* Check for a match */
      if (!string_is_empty(installed_core_name) &&
          string_is_equal(core_name, installed_core_name))
      {
         /* Must always 'release' the converted string */
         (*env)->ReleaseStringUTFChars(env,
               installed_core_name_jni, installed_core_name);
         return true;
      }

      /* Must always 'release' the converted string */
      (*env)->ReleaseStringUTFChars(env,
            installed_core_name_jni, installed_core_name);
   }

   return false;
}

/* Fetches last recorded status of the most
 * recently initiated play feature delivery
 * install transaction.
 * 'progress' is an integer from 0-100.
 * Returns true if a transaction is currently
 * in progress. */
bool play_feature_delivery_download_status(
      enum play_feature_delivery_install_status *status,
      unsigned *progress)
{
   play_feature_delivery_state_t* state = play_feature_delivery_get_state();
   bool active;

   /* Lock mutex */
#ifdef HAVE_THREADS
   slock_lock(state->status_lock);
#endif

   /* Copy status parameters */
   if (status)
      *status   = state->last_status;

   if (progress)
      *progress = state->download_progress;

   active       = state->active;

   /* Unlock mutex */
#ifdef HAVE_THREADS
   slock_unlock(state->status_lock);
#endif

   return active;
}

/***********/
/* Control */
/***********/

/* Initialises download of the specified core.
 * Returns false in the event of an error.
 * Download status should be monitored via
 * play_feature_delivery_download_status() */
bool play_feature_delivery_download(const char *core_file)
{
   play_feature_delivery_state_t* state = play_feature_delivery_get_state();
   JNIEnv *env                          = jni_thread_getenv();
   struct android_app *app              = (struct android_app*)g_android;
   bool success                         = false;
   char core_name[256];
   jstring core_name_jni;

   core_name[0] = '\0';

   if (!env ||
       !app ||
       !app->downloadCore)
      return false;

   /* Extract core name */
   if (!play_feature_delivery_get_core_name(
         core_file, core_name, sizeof(core_name)))
      return false;

   /* Lock mutex */
#ifdef HAVE_THREADS
   slock_lock(state->status_lock);
#endif

   /* We only support one download at a time */
   if (!state->active)
   {
      /* Update status */
      state->download_progress = 0;
      state->last_status       = PLAY_FEATURE_DELIVERY_PENDING;
      state->active            = true;
      strlcpy(state->last_core_name, core_name,
            sizeof(state->last_core_name));

      /* Convert core name to a Java-style string */
      core_name_jni = (*env)->NewStringUTF(env, core_name);

      /* Request download */
      CALL_VOID_METHOD_PARAM(env, app->activity->clazz,
            app->downloadCore, core_name_jni);

      /* Free core_name_jni reference */
      (*env)->DeleteLocalRef(env, core_name_jni);

      success = true;
   }

   /* Unlock mutex */
#ifdef HAVE_THREADS
   slock_unlock(state->status_lock);
#endif

   return success;
}

/* Deletes specified core.
 * Returns false in the event of an error. */
bool play_feature_delivery_delete(const char *core_file)
{
   JNIEnv *env             = jni_thread_getenv();
   struct android_app *app = (struct android_app*)g_android;
   char core_name[256];
   jstring core_name_jni;

   core_name[0] = '\0';

   if (!env ||
       !app ||
       !app->deleteCore)
      return false;

   /* Extract core name */
   if (!play_feature_delivery_get_core_name(
         core_file, core_name, sizeof(core_name)))
      return false;

   /* Convert to a Java-style string */
   core_name_jni = (*env)->NewStringUTF(env, core_name);

   /* Request core deletion */
   CALL_VOID_METHOD_PARAM(env, app->activity->clazz,
         app->deleteCore, core_name_jni);

   /* Free core_name_jni reference */
   (*env)->DeleteLocalRef(env, core_name_jni);

   return true;
}
