/*
 * Copyright (C) 2020 Endless OS Foundation, LLC
 *
 * 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; 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
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "clutter/clutter-blur-private.h"

#include "clutter/clutter-backend.h"

/**
 * ClutterBlur:
 *
 * Blur textures
 *
 * #ClutterBlur is a moderately fast gaussian blur implementation.
 *
 * # Optimizations
 *
 * There are a number of optimizations in place to make this blur implementation
 * real-time. All in all, the implementation performs best when using large
 * blur-radii that allow downscaling the texture to smaller sizes, at small
 * radii where no downscaling is possible this can easily halve the framerate.
 *
 * ## Multipass
 *
 * It is implemented in 2 passes: vertical and horizontal.
 *
 * ## Downscaling
 *
 * #ClutterBlur uses dynamic downscaling to speed up blurring. Downscaling
 * happens in factors of 2 (the image is downscaled either by 2, 4, 8, 16, …)
 * and depends on the blur radius, the texture size, among others.
 *
 * The texture is drawn into a downscaled framebuffer; the blur passes are
 * applied on the downscaled texture contents; and finally, the blurred
 * contents are drawn
 * upscaled again.
 *
 * ## Hardware Interpolation
 *
 * This blur implementation cuts down the number of sampling operations by
 * exploiting the hardware interpolation that is performed when sampling between
 * pixel boundaries. This technique is described at:
 *
 * http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
 *
 * ## Incremental gauss-factor calculation
 *
 * The kernel values for the gaussian kernel are computed incrementally instead
 * of running the expensive calculations multiple times inside the blur shader.
 * The implementation is based on the algorithm presented by K. Turkowski in
 * GPU Gems 3, chapter 40:
 *
 * https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch40.html
 *
 */

static const char *gaussian_blur_glsl_declarations =
"uniform float sigma;                                                      \n"
"uniform float pixel_step;                                                 \n"
"uniform vec2 direction;                                                   \n";

static const char *gaussian_blur_glsl =
"  vec2 uv = vec2 (cogl_tex_coord.st);                                     \n"
"                                                                          \n"
"  vec3 gauss_coefficient;                                                 \n"
"  gauss_coefficient.x = 1.0 / (sqrt (2.0 * 3.14159265) * sigma);          \n"
"  gauss_coefficient.y = exp (-0.5 / (sigma * sigma));                     \n"
"  gauss_coefficient.z = gauss_coefficient.y * gauss_coefficient.y;        \n"
"                                                                          \n"
"  float gauss_coefficient_total = gauss_coefficient.x;                    \n"
"                                                                          \n"
"  vec4 ret = texture2D (cogl_sampler, uv) * gauss_coefficient.x;          \n"
"  gauss_coefficient.xy *= gauss_coefficient.yz;                           \n"
"                                                                          \n"
"  int n_steps = int (ceil (1.5 * sigma)) * 2;                             \n"
"                                                                          \n"
"  for (int i = 1; i <= n_steps; i += 2) {                                 \n"
"    float coefficient_subtotal = gauss_coefficient.x;                     \n"
"    gauss_coefficient.xy *= gauss_coefficient.yz;                         \n"
"    coefficient_subtotal += gauss_coefficient.x;                          \n"
"                                                                          \n"
"    float gauss_ratio = gauss_coefficient.x / coefficient_subtotal;       \n"
"                                                                          \n"
"    float foffset = float (i) + gauss_ratio;                              \n"
"    vec2 offset = direction * foffset * pixel_step;                       \n"
"                                                                          \n"
"    ret += texture2D (cogl_sampler, uv + offset) * coefficient_subtotal;  \n"
"    ret += texture2D (cogl_sampler, uv - offset) * coefficient_subtotal;  \n"
"                                                                          \n"
"    gauss_coefficient_total += 2.0 * coefficient_subtotal;                \n"
"    gauss_coefficient.xy *= gauss_coefficient.yz;                         \n"
"  }                                                                       \n"
"                                                                          \n"
"  cogl_texel = ret / gauss_coefficient_total;                             \n";

#define MIN_DOWNSCALE_SIZE 256.f
#define MAX_SIGMA 6.f

enum
{
  VERTICAL,
  HORIZONTAL,
};

typedef struct
{
  CoglFramebuffer *framebuffer;
  CoglPipeline *pipeline;
  CoglTexture *texture;
  int orientation;
} BlurPass;

struct _ClutterBlur
{
  CoglTexture *source_texture;
  float sigma;
  float downscale_factor;

  BlurPass pass[2];
};

static CoglPipeline*
create_blur_pipeline (CoglContext *ctx)
{
  static CoglPipelineKey blur_pipeline_key = "clutter-blur-pipeline-private";
  CoglPipeline *blur_pipeline;

  blur_pipeline =
    cogl_context_get_named_pipeline (ctx, &blur_pipeline_key);

  if (G_UNLIKELY (blur_pipeline == NULL))
    {
      CoglSnippet *snippet;

      blur_pipeline = cogl_pipeline_new (ctx);
      cogl_pipeline_set_static_name (blur_pipeline, "ClutterBlur");
      cogl_pipeline_set_layer_null_texture (blur_pipeline, 0);
      cogl_pipeline_set_layer_filters (blur_pipeline,
                                       0,
                                       COGL_PIPELINE_FILTER_LINEAR,
                                       COGL_PIPELINE_FILTER_LINEAR);
      cogl_pipeline_set_layer_wrap_mode (blur_pipeline,
                                         0,
                                         COGL_PIPELINE_WRAP_MODE_CLAMP_TO_EDGE);

      snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_TEXTURE_LOOKUP,
                                  gaussian_blur_glsl_declarations,
                                  NULL);
      cogl_snippet_set_replace (snippet, gaussian_blur_glsl);
      cogl_pipeline_add_layer_snippet (blur_pipeline, 0, snippet);
      g_object_unref (snippet);

      cogl_context_set_named_pipeline (ctx, &blur_pipeline_key, blur_pipeline);
    }

  return cogl_pipeline_copy (blur_pipeline);
}

static void
update_blur_uniforms (ClutterBlur *blur,
                      BlurPass    *pass)
{
  gboolean vertical = pass->orientation == VERTICAL;
  int sigma_uniform;
  int pixel_step_uniform;
  int direction_uniform;

  pixel_step_uniform =
    cogl_pipeline_get_uniform_location (pass->pipeline, "pixel_step");
  if (pixel_step_uniform > -1)
    {
      float pixel_step;

      if (vertical)
        pixel_step = 1.f / cogl_texture_get_height (pass->texture);
      else
        pixel_step = 1.f / cogl_texture_get_width (pass->texture);

      cogl_pipeline_set_uniform_1f (pass->pipeline,
                                    pixel_step_uniform,
                                    pixel_step);
    }

  sigma_uniform = cogl_pipeline_get_uniform_location (pass->pipeline, "sigma");
  if (sigma_uniform > -1)
    {
      cogl_pipeline_set_uniform_1f (pass->pipeline,
                                    sigma_uniform,
                                    blur->sigma / blur->downscale_factor);
    }

  direction_uniform =
    cogl_pipeline_get_uniform_location (pass->pipeline, "direction");
  if (direction_uniform > -1)
    {
      gboolean horizontal = !vertical;
      float direction[2] = {
        horizontal,
        vertical,
      };

      cogl_pipeline_set_uniform_float (pass->pipeline,
                                       direction_uniform,
                                       2, 1,
                                       direction);
    }
}

static gboolean
create_fbo (ClutterBlur *blur,
            CoglContext *ctx,
            BlurPass    *pass)
{
  float scaled_height;
  float scaled_width;
  float height;
  float width;

  g_clear_object (&pass->texture);
  g_clear_object (&pass->framebuffer);

  width = cogl_texture_get_width (blur->source_texture);
  height = cogl_texture_get_height (blur->source_texture);
  scaled_width = floorf (width / blur->downscale_factor);
  scaled_height = floorf (height / blur->downscale_factor);

  pass->texture = cogl_texture_2d_new_with_size (ctx,
                                                 (int) scaled_width,
                                                 (int) scaled_height);
  if (!pass->texture)
    return FALSE;

  pass->framebuffer =
    COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (pass->texture));
  if (!pass->framebuffer)
    {
      g_warning ("%s: Unable to create an Offscreen buffer", G_STRLOC);
      return FALSE;
    }

  cogl_framebuffer_orthographic (pass->framebuffer,
                                 0.0, 0.0,
                                 scaled_width,
                                 scaled_height,
                                 0.0, 1.0);
  return TRUE;
}

static gboolean
setup_blur_pass (ClutterBlur *blur,
                 BlurPass    *pass,
                 int          orientation,
                 CoglTexture *texture)
{
  CoglContext *context = cogl_texture_get_context (texture);
  pass->orientation = orientation;
  pass->pipeline = create_blur_pipeline (context);
  cogl_pipeline_set_layer_texture (pass->pipeline, 0, texture);

  if (!create_fbo (blur, context, pass))
    return FALSE;

  update_blur_uniforms (blur, pass);
  return TRUE;
}

static float
calculate_downscale_factor (float width,
                            float height,
                            float sigma)
{
  float downscale_factor = 1.f;
  float scaled_width = width;
  float scaled_height = height;
  float scaled_sigma = sigma;

  /* This is the algorithm used by Firefox; keep downscaling until either the
   * blur radius is lower than the threshold, or the downscaled texture is too
   * small.
   */
  while (scaled_sigma > MAX_SIGMA &&
         scaled_width > MIN_DOWNSCALE_SIZE &&
         scaled_height > MIN_DOWNSCALE_SIZE)
    {
      downscale_factor *= 2.f;

      scaled_width = width / downscale_factor;
      scaled_height = height / downscale_factor;
      scaled_sigma = sigma / downscale_factor;
    }

  return downscale_factor;
}

static void
apply_blur_pass (BlurPass *pass)
{
  CoglColor transparent;

  cogl_color_init_from_4f (&transparent, 0.0, 0.0, 0.0, 0.0);

  cogl_framebuffer_clear (pass->framebuffer,
                          COGL_BUFFER_BIT_COLOR,
                          &transparent);

  cogl_framebuffer_draw_rectangle (pass->framebuffer,
                                   pass->pipeline,
                                   0, 0,
                                   cogl_texture_get_width (pass->texture),
                                   cogl_texture_get_height (pass->texture));
}

static void
clear_blur_pass (BlurPass *pass)
{
  g_clear_object (&pass->pipeline);
  g_clear_object (&pass->texture);
  g_clear_object (&pass->framebuffer);
}

/**
 * clutter_blur_new:
 * @texture: a #CoglTexture
 * @sigma: blur sigma
 *
 * Creates a new #ClutterBlur.
 *
 * Returns: (transfer full) (nullable): A newly created #ClutterBlur
 */
ClutterBlur *
clutter_blur_new (CoglTexture *texture,
                  float        radius)
{
  ClutterBlur *blur;
  unsigned int height;
  unsigned int width;
  BlurPass *hpass;
  BlurPass *vpass;

  g_return_val_if_fail (texture != NULL, NULL);
  g_return_val_if_fail (radius >= 0.0f, NULL);

  width = cogl_texture_get_width (texture);
  height = cogl_texture_get_height (texture);

  blur = g_new0 (ClutterBlur, 1);
  blur->sigma = radius / 2.0f;
  blur->source_texture = g_object_ref (texture);
  blur->downscale_factor = calculate_downscale_factor (width,
                                                       height,
                                                       blur->sigma);

  if (G_APPROX_VALUE (blur->sigma, 0.0f, FLT_EPSILON))
    goto out;

  vpass = &blur->pass[VERTICAL];
  hpass = &blur->pass[HORIZONTAL];

  if (!setup_blur_pass (blur, vpass, VERTICAL, texture) ||
      !setup_blur_pass (blur, hpass, HORIZONTAL, vpass->texture))
    {
      clutter_blur_free (blur);
      return NULL;
    }

out:
  return g_steal_pointer (&blur);
}

/**
 * clutter_blur_apply:
 * @blur: a #ClutterBlur
 *
 * Applies the blur. The resulting texture can be retrieved by
 * [method@Clutter.Blur.get_texture].
 */
void
clutter_blur_apply (ClutterBlur *blur)
{
  if (G_APPROX_VALUE (blur->sigma, 0.0, FLT_EPSILON))
    return;

  apply_blur_pass (&blur->pass[VERTICAL]);
  apply_blur_pass (&blur->pass[HORIZONTAL]);
}

/**
 * clutter_blur_get_texture:
 * @blur: a #ClutterBlur
 *
 * Retrieves the texture where the blurred contents are stored. The
 * contents are undefined until [method@Clutter.Blur.apply] is called.
 *
 * Returns: (transfer none): a #CoglTexture
 */
CoglTexture *
clutter_blur_get_texture (ClutterBlur *blur)
{
  if (G_APPROX_VALUE (blur->sigma, 0.0, FLT_EPSILON))
    return blur->source_texture;
  else
    return blur->pass[HORIZONTAL].texture;
}

/**
 * clutter_blur_free:
 * @blur: A #ClutterBlur
 *
 * Frees @blur.
 */
void
clutter_blur_free (ClutterBlur *blur)
{
  g_assert (blur);

  clear_blur_pass (&blur->pass[VERTICAL]);
  clear_blur_pass (&blur->pass[HORIZONTAL]);
  g_clear_object (&blur->source_texture);
  g_free (blur);
}
