/*
 * Copyright (C) 2013, Fluendo S.A.
 *   Author: Andoni Morales <amorales@fluendo.com>
 *
 * Copyright (C) 2014,2018 Collabora Ltd.
 *   Author: Matthieu Bouron <matthieu.bouron@collabora.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation
 * version 2.1 of the License.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "gstjniutils.h"
#include "gstamcsurfacetexture-jni.h"

struct _GstAmcSurfaceTextureJNI
{
  GstAmcSurfaceTexture parent_instance;
  jobject jobject;
  gint texture_id;

  jobject listener;
  jmethodID set_context_id;
  GstAmcSurfaceTextureOnFrameAvailableCallback callback;
  gpointer user_data;
};

static struct
{
  jclass jklass;
  jmethodID constructor;
  jmethodID set_on_frame_available_listener;
  jmethodID update_tex_image;
  jmethodID detach_from_gl_context;
  jmethodID attach_to_gl_context;
  jmethodID get_transform_matrix;
  jmethodID get_timestamp;
  jmethodID release;
} surface_texture;


G_DEFINE_TYPE (GstAmcSurfaceTextureJNI, gst_amc_surface_texture_jni,
    GST_TYPE_AMC_SURFACE_TEXTURE);

gboolean
gst_amc_surface_texture_static_init (void)
{
  JNIEnv *env;
  GError *err = NULL;

  env = gst_amc_jni_get_env ();

  surface_texture.jklass = gst_amc_jni_get_class (env, &err,
      "android/graphics/SurfaceTexture");
  if (!surface_texture.jklass) {
    GST_ERROR ("Failed to get android.graphics.SurfaceTexture class: %s",
        err->message);
    g_clear_error (&err);
    return FALSE;
  }

  surface_texture.constructor =
      gst_amc_jni_get_method_id (env, &err, surface_texture.jklass, "<init>",
      "(I)V");
  if (!surface_texture.constructor) {
    goto error;
  }

  surface_texture.set_on_frame_available_listener =
      gst_amc_jni_get_method_id (env, &err, surface_texture.jklass,
      "setOnFrameAvailableListener",
      "(Landroid/graphics/SurfaceTexture$OnFrameAvailableListener;)V");
  if (!surface_texture.set_on_frame_available_listener) {
    goto error;
  }

  surface_texture.update_tex_image =
      gst_amc_jni_get_method_id (env, &err, surface_texture.jklass,
      "updateTexImage", "()V");
  if (!surface_texture.update_tex_image) {
    goto error;
  }

  surface_texture.detach_from_gl_context =
      gst_amc_jni_get_method_id (env, &err, surface_texture.jklass,
      "detachFromGLContext", "()V");
  if (!surface_texture.detach_from_gl_context) {
    goto error;
  }

  surface_texture.attach_to_gl_context =
      gst_amc_jni_get_method_id (env, &err, surface_texture.jklass,
      "attachToGLContext", "(I)V");
  if (!surface_texture.attach_to_gl_context) {
    goto error;
  }

  surface_texture.get_transform_matrix =
      gst_amc_jni_get_method_id (env, &err, surface_texture.jklass,
      "getTransformMatrix", "([F)V");
  if (!surface_texture.get_transform_matrix) {
    goto error;
  }

  surface_texture.get_timestamp =
      gst_amc_jni_get_method_id (env, &err, surface_texture.jklass,
      "getTimestamp", "()J");
  if (!surface_texture.get_timestamp) {
    goto error;
  }

  surface_texture.release =
      gst_amc_jni_get_method_id (env, &err, surface_texture.jklass, "release",
      "()V");
  if (!surface_texture.release) {
    goto error;
  }

  return TRUE;

error:
  GST_ERROR ("Failed to get android.graphics.SurfaceTexture methods: %s",
      err->message);
  g_clear_error (&err);
  gst_amc_jni_object_unref (env, surface_texture.constructor);
  return FALSE;
}

static gboolean
gst_amc_surface_texture_jni_update_tex_image (GstAmcSurfaceTexture * base,
    GError ** err)
{
  GstAmcSurfaceTextureJNI *self = GST_AMC_SURFACE_TEXTURE_JNI (base);
  JNIEnv *env;

  env = gst_amc_jni_get_env ();

  return gst_amc_jni_call_void_method (env, err, self->jobject,
      surface_texture.update_tex_image);
}

static gboolean
gst_amc_surface_texture_jni_detach_from_gl_context (GstAmcSurfaceTexture * base,
    GError ** err)
{
  GstAmcSurfaceTextureJNI *self = GST_AMC_SURFACE_TEXTURE_JNI (base);
  JNIEnv *env;
  gboolean ret;

  env = gst_amc_jni_get_env ();

  ret =
      gst_amc_jni_call_void_method (env, err, self->jobject,
      surface_texture.detach_from_gl_context);
  self->texture_id = 0;
  return ret;
}

static gboolean
gst_amc_surface_texture_jni_attach_to_gl_context (GstAmcSurfaceTexture * base,
    gint texture_id, GError ** err)
{
  GstAmcSurfaceTextureJNI *self = GST_AMC_SURFACE_TEXTURE_JNI (base);
  JNIEnv *env;
  gboolean ret;

  env = gst_amc_jni_get_env ();

  ret =
      gst_amc_jni_call_void_method (env, err, self->jobject,
      surface_texture.attach_to_gl_context, texture_id);
  self->texture_id = texture_id;
  return ret;
}

static gboolean
gst_amc_surface_texture_jni_get_transform_matrix (GstAmcSurfaceTexture * base,
    gfloat * matrix, GError ** err)
{
  GstAmcSurfaceTextureJNI *self = GST_AMC_SURFACE_TEXTURE_JNI (base);
  JNIEnv *env;
  gboolean ret;
  /* 4x4 Matrix */
  jsize size = 16;
  jfloatArray floatarray;

  env = gst_amc_jni_get_env ();

  floatarray = (*env)->NewFloatArray (env, size);
  ret =
      gst_amc_jni_call_void_method (env, err, self->jobject,
      surface_texture.get_transform_matrix, floatarray);
  if (ret) {
    (*env)->GetFloatArrayRegion (env, floatarray, 0, size, (jfloat *) matrix);
    (*env)->DeleteLocalRef (env, floatarray);
  }

  return ret;
}

static gboolean
gst_amc_surface_texture_jni_get_timestamp (GstAmcSurfaceTexture * base,
    gint64 * result, GError ** err)
{
  GstAmcSurfaceTextureJNI *self = GST_AMC_SURFACE_TEXTURE_JNI (base);
  JNIEnv *env;

  env = gst_amc_jni_get_env ();

  return gst_amc_jni_call_long_method (env, err, self->jobject,
      surface_texture.get_timestamp, result);
}

static gboolean
gst_amc_surface_texture_jni_release (GstAmcSurfaceTexture * base, GError ** err)
{
  GstAmcSurfaceTextureJNI *self = GST_AMC_SURFACE_TEXTURE_JNI (base);
  JNIEnv *env;

  env = gst_amc_jni_get_env ();

  return gst_amc_jni_call_void_method (env, err, self->jobject,
      surface_texture.release);
}

static void
on_frame_available_cb (JNIEnv * env, jobject thiz,
    long long context, jobject surfaceTexture)
{
  GstAmcSurfaceTextureJNI *self = JLONG_TO_GPOINTER (context);

  self->callback (GST_AMC_SURFACE_TEXTURE (self), self->user_data);
}

static gboolean
create_listener (GstAmcSurfaceTextureJNI * self, JNIEnv * env, GError ** err)
{
  jclass listener_cls = NULL;
  jmethodID constructor_id = 0;

  JNINativeMethod amcOnFrameAvailableListener = {
    "native_onFrameAvailable",
    "(JLandroid/graphics/SurfaceTexture;)V",
    (void *) on_frame_available_cb,
  };

  listener_cls =
      gst_amc_jni_get_application_class (env,
      "org/freedesktop/gstreamer/androidmedia/GstAmcOnFrameAvailableListener",
      err);
  if (!listener_cls) {
    return FALSE;
  }

  (*env)->RegisterNatives (env, listener_cls, &amcOnFrameAvailableListener, 1);
  if ((*env)->ExceptionCheck (env)) {
    gst_amc_jni_set_error (env, err, GST_LIBRARY_ERROR,
        GST_LIBRARY_ERROR_FAILED, "Failed to register native methods");
    goto done;
  }

  constructor_id =
      gst_amc_jni_get_method_id (env, err, listener_cls, "<init>", "()V");
  if (!constructor_id) {
    goto done;
  }

  self->set_context_id =
      gst_amc_jni_get_method_id (env, err, listener_cls, "setContext", "(J)V");
  if (!self->set_context_id) {
    goto done;
  }

  self->listener =
      gst_amc_jni_new_object (env, err, TRUE, listener_cls, constructor_id);
  if (!self->listener) {
    goto done;
  }

  if (!gst_amc_jni_call_void_method (env, err, self->listener,
          self->set_context_id, GPOINTER_TO_JLONG (self))) {
    gst_amc_jni_object_unref (env, self->listener);
    self->listener = NULL;
  }

done:
  gst_amc_jni_object_unref (env, listener_cls);

  return self->listener != NULL;
}

static gboolean
remove_listener (GstAmcSurfaceTextureJNI * self, JNIEnv * env, GError ** err)
{
  if (self->listener) {
    if (!gst_amc_jni_call_void_method (env, err, self->listener,
            self->set_context_id, GPOINTER_TO_JLONG (NULL)))
      return FALSE;

    gst_amc_jni_object_unref (env, self->listener);
    self->listener = NULL;
  }

  return TRUE;
}

static gboolean
    gst_amc_surface_texture_jni_set_on_frame_available_callback
    (GstAmcSurfaceTexture * base,
    GstAmcSurfaceTextureOnFrameAvailableCallback callback, gpointer user_data,
    GError ** err)
{
  GstAmcSurfaceTextureJNI *self = GST_AMC_SURFACE_TEXTURE_JNI (base);
  JNIEnv *env;
  GError *local_error = NULL;

  env = gst_amc_jni_get_env ();

  if (!remove_listener (self, env, err))
    return FALSE;

  self->callback = callback;
  self->user_data = user_data;
  if (callback == NULL)
    return TRUE;

  if (!create_listener (self, env, &local_error)) {
    GST_ERROR ("Could not create listener: %s", local_error->message);
    g_propagate_error (err, local_error);
    return FALSE;
  }

  if (!gst_amc_jni_call_void_method (env, err, self->jobject,
          surface_texture.set_on_frame_available_listener, self->listener)) {
    remove_listener (self, env, NULL);
    return FALSE;
  }

  return TRUE;
}

static void
gst_amc_surface_texture_jni_dispose (GObject * object)
{
  GstAmcSurfaceTextureJNI *self = GST_AMC_SURFACE_TEXTURE_JNI (object);
  JNIEnv *env;
  GError *err = NULL;

  env = gst_amc_jni_get_env ();

  if (!gst_amc_surface_texture_jni_release (GST_AMC_SURFACE_TEXTURE (self),
          &err)) {
    GST_ERROR ("Could not release surface texture: %s", err->message);
    g_clear_error (&err);
  }

  remove_listener (self, env, NULL);

  if (self->jobject) {
    gst_amc_jni_object_unref (env, self->jobject);
  }

  G_OBJECT_CLASS (gst_amc_surface_texture_jni_parent_class)->dispose (object);
}

static void
gst_amc_surface_texture_jni_class_init (GstAmcSurfaceTextureJNIClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstAmcSurfaceTextureClass *surface_texture_class =
      GST_AMC_SURFACE_TEXTURE_CLASS (klass);

  gobject_class->dispose = gst_amc_surface_texture_jni_dispose;

  surface_texture_class->update_tex_image =
      gst_amc_surface_texture_jni_update_tex_image;
  surface_texture_class->detach_from_gl_context =
      gst_amc_surface_texture_jni_detach_from_gl_context;
  surface_texture_class->attach_to_gl_context =
      gst_amc_surface_texture_jni_attach_to_gl_context;
  surface_texture_class->get_transform_matrix =
      gst_amc_surface_texture_jni_get_transform_matrix;
  surface_texture_class->get_timestamp =
      gst_amc_surface_texture_jni_get_timestamp;
  surface_texture_class->release = gst_amc_surface_texture_jni_release;
  surface_texture_class->set_on_frame_available_callback =
      gst_amc_surface_texture_jni_set_on_frame_available_callback;
}

static void
gst_amc_surface_texture_jni_init (GstAmcSurfaceTextureJNI * self)
{
}

GstAmcSurfaceTextureJNI *
gst_amc_surface_texture_jni_new (GError ** err)
{
  GstAmcSurfaceTextureJNI *self = NULL;
  JNIEnv *env;

  self = g_object_new (GST_TYPE_AMC_SURFACE_TEXTURE_JNI, NULL);
  env = gst_amc_jni_get_env ();

  self->texture_id = 0;

  self->jobject =
      gst_amc_jni_new_object (env, err, TRUE, surface_texture.jklass,
      surface_texture.constructor, self->texture_id);
  if (self->jobject == NULL) {
    goto error;
  }

  if (!gst_amc_surface_texture_jni_detach_from_gl_context ((GstAmcSurfaceTexture
              *) self, err)) {
    goto error;
  }

  return self;

error:
  if (self)
    g_object_unref (self);
  return NULL;
}

jobject
gst_amc_surface_texture_jni_get_jobject (GstAmcSurfaceTextureJNI * self)
{
  return self->jobject;
}