/*
 * GStreamer
 * Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

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

#include <gst/gl/gl.h>

#include "gstglslstage.h"
#include "gstglsl_private.h"

/**
 * SECTION:gstglslstage
 * @short_description: object for dealing with OpenGL shader stages
 * @title: GstGLSLStage
 * @see_also: #GstGLShader
 *
 * #GstGLSLStage holds and represents a single OpenGL shader stage.
 */

static const gchar *es2_version_header = "#version 100\n";

GST_DEBUG_CATEGORY_STATIC (gst_glsl_stage_debug);
#define GST_CAT_DEFAULT gst_glsl_stage_debug

G_DEFINE_TYPE_WITH_CODE (GstGLSLStage, gst_glsl_stage, GST_TYPE_OBJECT,
    GST_DEBUG_CATEGORY_INIT (gst_glsl_stage_debug, "glslstage", 0,
        "GLSL Stage");
    );

#define GST_GLSL_STAGE_GET_PRIVATE(o) \
  (G_TYPE_INSTANCE_GET_PRIVATE((o), GST_TYPE_GLSL_STAGE, GstGLSLStagePrivate))

struct _GstGLSLStagePrivate
{
  GstGLSLFuncs vtable;

  GLenum type;
  GLhandleARB handle;
  GstGLSLVersion version;
  GstGLSLProfile profile;
  gchar **strings;
  gint n_strings;

  gboolean compiled;
};

static void
gst_glsl_stage_finalize (GObject * object)
{
  GstGLSLStage *stage = GST_GLSL_STAGE (object);
  gint i;

  if (stage->context) {
    gst_object_unref (stage->context);
    stage->context = NULL;
  }

  for (i = 0; i < stage->priv->n_strings; i++) {
    g_free (stage->priv->strings[i]);
  }
  g_free (stage->priv->strings);
  stage->priv->strings = NULL;

  G_OBJECT_CLASS (gst_glsl_stage_parent_class)->finalize (object);
}

static void
gst_glsl_stage_set_property (GObject * object,
    guint prop_id, const GValue * value, GParamSpec * pspec)
{
  switch (prop_id) {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_glsl_stage_get_property (GObject * object,
    guint prop_id, GValue * value, GParamSpec * pspec)
{
  switch (prop_id) {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }

}

static void
gst_glsl_stage_class_init (GstGLSLStageClass * klass)
{
  GObjectClass *obj_class = G_OBJECT_CLASS (klass);

  g_type_class_add_private (klass, sizeof (GstGLSLStagePrivate));

  obj_class->finalize = gst_glsl_stage_finalize;
  obj_class->set_property = gst_glsl_stage_set_property;
  obj_class->get_property = gst_glsl_stage_get_property;
}

static void
gst_glsl_stage_init (GstGLSLStage * stage)
{
  stage->priv = GST_GLSL_STAGE_GET_PRIVATE (stage);
}

static gboolean
_is_valid_shader_type (GLenum type)
{
  switch (type) {
    case GL_VERTEX_SHADER:
    case GL_FRAGMENT_SHADER:
#ifdef GL_TESS_CONTROL_SHADER
    case GL_TESS_CONTROL_SHADER:
#endif
#ifdef GL_TESS_EVALUATION_SHADER
    case GL_TESS_EVALUATION_SHADER:
#endif
#ifdef GL_GEOMETRY_SHADER
    case GL_GEOMETRY_SHADER:
#endif
#ifdef GL_COMPUTE_SHADER
    case GL_COMPUTE_SHADER:
#endif
      return TRUE;
    default:
      return FALSE;
  }
}

static const gchar *
_shader_type_to_string (GLenum type)
{
  switch (type) {
    case GL_VERTEX_SHADER:
      return "vertex";
    case GL_FRAGMENT_SHADER:
      return "fragment";
#ifdef GL_TESS_CONTROL_SHADER
    case GL_TESS_CONTROL_SHADER:
      return "tesselation control";
#endif
#ifdef GL_TESS_EVALUATION_SHADER
    case GL_TESS_EVALUATION_SHADER:
      return "tesselation evaluation";
#endif
#ifdef GL_GEOMETRY_SHADER
    case GL_GEOMETRY_SHADER:
      return "geometry";
#endif
#ifdef GL_COMPUTE_SHADER
    case GL_COMPUTE_SHADER:
      return "compute";
#endif
    default:
      return "unknown";
  }
}

static gboolean
_ensure_shader (GstGLSLStage * stage)
{
  if (stage->priv->handle)
    return TRUE;

  if (!(stage->priv->handle =
          stage->priv->vtable.CreateShader (stage->priv->type)))
    return FALSE;

  return stage->priv->handle != 0;
}

/**
 * gst_glsl_stage_new_with_strings:
 * @context: a #GstGLContext
 * @type: the GL enum shader stage type
 * @version: the #GstGLSLVersion
 * @profile: the #GstGLSLProfile
 * @n_strings: the number of strings in @str
 * @str: an array of strings concatted together to produce a shader
 *
 * Returns: (transfer full): a new #GstGLSLStage of the specified @type
 */
GstGLSLStage *
gst_glsl_stage_new_with_strings (GstGLContext * context, guint type,
    GstGLSLVersion version, GstGLSLProfile profile, gint n_strings,
    const gchar ** str)
{
  GstGLSLStage *stage;

  g_return_val_if_fail (GST_IS_GL_CONTEXT (context), NULL);
  g_return_val_if_fail (_is_valid_shader_type (type), NULL);

  stage = g_object_new (GST_TYPE_GLSL_STAGE, NULL);
  /* FIXME: GInittable */
  if (!_gst_glsl_funcs_fill (&stage->priv->vtable, context)) {
    gst_object_unref (stage);
    return NULL;
  }

  stage->context = gst_object_ref (context);
  stage->priv->type = type;
  if (!gst_glsl_stage_set_strings (stage, version, profile, n_strings, str)) {
    gst_object_unref (stage);
    return NULL;
  }

  return stage;
}

/**
 * gst_glsl_stage_new_with_string:
 * @context: a #GstGLContext
 * @type: the GL enum shader stage type
 * @version: the #GstGLSLVersion
 * @profile: the #GstGLSLProfile
 * @str: a shader string
 *
 * Returns: (transfer full): a new #GstGLSLStage of the specified @type
 */
GstGLSLStage *
gst_glsl_stage_new_with_string (GstGLContext * context, guint type,
    GstGLSLVersion version, GstGLSLProfile profile, const gchar * str)
{
  return gst_glsl_stage_new_with_strings (context, type, version, profile, 1,
      &str);
}

/**
 * gst_glsl_stage_new:
 * @context: a #GstGLContext
 * @type: the GL enum shader stage type
 *
 * Returns: (transfer full): a new #GstGLSLStage of the specified @type
 */
GstGLSLStage *
gst_glsl_stage_new (GstGLContext * context, guint type)
{
  return gst_glsl_stage_new_with_string (context, type, GST_GLSL_VERSION_NONE,
      GST_GLSL_PROFILE_NONE, NULL);
}

/**
 * gst_glsl_stage_new_with_default_vertex:
 * @context: a #GstGLContext
 *
 * Returns: (transfer full): a new #GstGLSLStage with the default vertex shader
 */
GstGLSLStage *
gst_glsl_stage_new_default_vertex (GstGLContext * context)
{
  return gst_glsl_stage_new_with_string (context, GL_VERTEX_SHADER,
      GST_GLSL_VERSION_NONE,
      GST_GLSL_PROFILE_ES | GST_GLSL_PROFILE_COMPATIBILITY,
      gst_gl_shader_string_vertex_default);
}

/**
 * gst_glsl_stage_new_with_default_fragment:
 * @context: a #GstGLContext
 *
 * Returns: (transfer full): a new #GstGLSLStage with the default fragment shader
 */
GstGLSLStage *
gst_glsl_stage_new_default_fragment (GstGLContext * context)
{
  return gst_glsl_stage_new_with_string (context, GL_FRAGMENT_SHADER,
      GST_GLSL_VERSION_NONE,
      GST_GLSL_PROFILE_ES | GST_GLSL_PROFILE_COMPATIBILITY,
      gst_gl_shader_string_fragment_default);
}

/**
 * gst_glsl_stage_set_strings:
 * @stage: a #GstGLSLStage
 * @version: a #GstGLSLVersion
 * @profile: a #GstGLSLProfile
 * @n_strings: number of strings in @str
 * @str: (transfer none): a GLSL shader string
 *
 * Replaces the current shader string with @str.
 */
gboolean
gst_glsl_stage_set_strings (GstGLSLStage * stage, GstGLSLVersion version,
    GstGLSLProfile profile, gint n_strings, const gchar ** str)
{
  gint i;

  g_return_val_if_fail (GST_IS_GLSL_STAGE (stage), FALSE);
  g_return_val_if_fail (n_strings > 0, FALSE);
  g_return_val_if_fail (str != NULL, FALSE);

  if (!gst_gl_context_supports_glsl_profile_version (stage->context, version,
          profile)) {
    const gchar *version_str = gst_glsl_version_to_string (version);
    const gchar *profile_str = gst_glsl_profile_to_string (profile);
    GST_ERROR_OBJECT (stage, "GL context does not support version %s and "
        "profile %s", version_str, profile_str);
    return FALSE;
  }

  stage->priv->version = version;
  stage->priv->profile = profile;

  for (i = 0; i < stage->priv->n_strings; i++) {
    g_free (stage->priv->strings[i]);
  }

  if (stage->priv->n_strings < n_strings) {
    /* only realloc if we need more space */
    g_free (stage->priv->strings);
    stage->priv->strings = g_new0 (gchar *, n_strings);
  }

  for (i = 0; i < n_strings; i++)
    stage->priv->strings[i] = g_strdup (str[i]);
  stage->priv->n_strings = n_strings;

  return TRUE;
}

/**
 * gst_glsl_stage_get_shader_type:
 * @stage: a #GstGLSLStage
 *
 * Returns: The GL shader type for this shader stage
 */
guint
gst_glsl_stage_get_shader_type (GstGLSLStage * stage)
{
  g_return_val_if_fail (GST_IS_GLSL_STAGE (stage), 0);

  return stage->priv->type;
}

/**
 * gst_glsl_stage_get_handle:
 * @stage: a #GstGLSLStage
 *
 * Returns: The GL handle for this shader stage
 */
guint
gst_glsl_stage_get_handle (GstGLSLStage * stage)
{
  g_return_val_if_fail (GST_IS_GLSL_STAGE (stage), 0);
  g_return_val_if_fail (stage->priv->compiled, 0);

  return stage->priv->handle;
}

/**
 * gst_glsl_stage_get_version:
 * @stage: a #GstGLSLStage
 *
 * Returns: The GLSL version for the current shader stage
 */
GstGLSLVersion
gst_glsl_stage_get_version (GstGLSLStage * stage)
{
  g_return_val_if_fail (GST_IS_GLSL_STAGE (stage), 0);

  return stage->priv->version;
}

/**
 * gst_glsl_stage_get_profile:
 * @stage: a #GstGLSLStage
 *
 * Returns: The GLSL profile for the current shader stage
 */
GstGLSLProfile
gst_glsl_stage_get_profile (GstGLSLStage * stage)
{
  g_return_val_if_fail (GST_IS_GLSL_STAGE (stage), 0);

  return stage->priv->profile;
}

static void
_maybe_prepend_version (GstGLSLStage * stage, gchar ** shader_str,
    gint * n_vertex_sources, const gchar *** vertex_sources)
{
  gint n = *n_vertex_sources;
  gboolean add_header = FALSE;
  gint i, j;

  /* FIXME: this all an educated guess */
  if (gst_gl_context_check_gl_version (stage->context, GST_GL_API_OPENGL3, 3, 0)
      && (stage->priv->profile & GST_GLSL_PROFILE_ES) != 0
      && !_gst_glsl_shader_string_find_version (shader_str[0])) {
    add_header = TRUE;
    n++;
  }

  *vertex_sources = g_malloc0 (n * sizeof (gchar *));

  i = 0;
  if (add_header)
    (*vertex_sources)[i++] = es2_version_header;

  for (j = 0; j < stage->priv->n_strings; i++, j++)
    (*vertex_sources)[i] = shader_str[j];
  *n_vertex_sources = n;
}

struct compile
{
  GstGLSLStage *stage;
  GError **error;
  gboolean result;
};

static void
_compile_shader (GstGLContext * context, struct compile *data)
{
  GstGLSLStagePrivate *priv = data->stage->priv;
  GstGLSLFuncs *vtable = &data->stage->priv->vtable;
  const GstGLFuncs *gl = context->gl_vtable;
  const gchar **vertex_sources;
  gchar info_buffer[2048];
  gint n_vertex_sources;
  GLint status;
  gint len;
  gint i;

  if (data->stage->priv->compiled) {
    data->result = TRUE;
    return;
  }

  if (!_ensure_shader (data->stage)) {
    g_set_error (data->error, GST_GLSL_ERROR, GST_GLSL_ERROR_COMPILE,
        "Failed to create shader object");
    data->result = FALSE;
    return;
  }

  n_vertex_sources = data->stage->priv->n_strings;
  _maybe_prepend_version (data->stage, priv->strings, &n_vertex_sources,
      &vertex_sources);

  GST_TRACE_OBJECT (data->stage, "compiling shader:");
  for (i = 0; i < n_vertex_sources; i++) {
    GST_TRACE_OBJECT (data->stage, "%s", vertex_sources[i]);
  }

  gl->ShaderSource (priv->handle, n_vertex_sources,
      (const gchar **) vertex_sources, NULL);
  gl->CompileShader (priv->handle);
  g_free (vertex_sources);
  /* FIXME: supported threaded GLSL compilers and don't destroy compilation
   * performance by getting the compilation result directly after compilation */
  status = GL_FALSE;
  vtable->GetShaderiv (priv->handle, GL_COMPILE_STATUS, &status);

  len = 0;
  vtable->GetShaderInfoLog (priv->handle, sizeof (info_buffer) - 1, &len,
      info_buffer);
  info_buffer[len] = '\0';

  if (status != GL_TRUE) {
    GST_ERROR_OBJECT (data->stage, "%s shader compilation failed:%s",
        _shader_type_to_string (priv->type), info_buffer);

    g_set_error (data->error, GST_GLSL_ERROR, GST_GLSL_ERROR_COMPILE,
        "%s shader compilation failed:%s",
        _shader_type_to_string (priv->type), info_buffer);

    vtable->DeleteShader (priv->handle);
    data->result = FALSE;
    return;
  } else if (len > 1) {
    GST_FIXME_OBJECT (data->stage, "%s shader info log:%s",
        _shader_type_to_string (priv->type), info_buffer);
  }

  data->result = TRUE;
}

/**
 * gst_glsl_stage_compile:
 * @stage: a #GstGLSLStage
 * @error: a #GError to use on failure
 *
 * Returns: whether the compilation suceeded
 */
gboolean
gst_glsl_stage_compile (GstGLSLStage * stage, GError ** error)
{
  struct compile data;

  g_return_val_if_fail (GST_IS_GLSL_STAGE (stage), FALSE);

  if (!stage->priv->strings) {
    g_set_error (error, GST_GLSL_ERROR, GST_GLSL_ERROR_COMPILE,
        "No shader source to compile");
    return FALSE;
  }

  data.stage = stage;
  data.error = error;

  gst_gl_context_thread_add (stage->context,
      (GstGLContextThreadFunc) _compile_shader, &data);

  stage->priv->compiled = TRUE;

  return data.result;
}
