/*
 *  Copyright 1994-2022 Olivier Girondel
 *  Copyright 2014-2022 Frantz Balinski
 *  Copyright 2019-2022 Laurent Marsac
 *
 *  This file is part of lebiniou.
 *
 *  lebiniou 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 Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  lebiniou 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 lebiniou. If not, see <http://www.gnu.org/licenses/>.
 */

#include "constants.h"
#include "context.h"
#include "parameters.h"
#include "pthread_utils.h"


uint32_t version = 0;
uint32_t options = BO_SFX | BO_SCHEMES;
enum LayerMode mode = LM_OVERLAY;
char desc[] = "Pulsing spheres";
char dname[] = "Spheres pulse";


/*
 * Optimisation:
 * Spheres color indices are precomputed.
 */


typedef struct position {
  uint16_t x, y;
} POSITION;

/* spheres centres */
static POSITION *centres = NULL;

/* sphere radius */
static uint16_t radius;

/* max radius */
static uint16_t radius_max;

/* size maximum of color indices grid */
static uint16_t size_max;

/* color indices grid */
static Pixel_t *color_indices = NULL;

/* parameters */
static int nb_spheres = 16;
static double radius_factor = 0.04; /* sets radius max */
static double volume_scale = 1;
static double sensitivity = 3;
static double move_factor = 1;
static double border_x = 0; /* border will be set to border_x * HWIDTH */
static double border_y = 0; /* border will be set to border_y * HHEIGHT */
static pthread_mutex_t mutex;


static void free_spheres(void);
static void move_spheres(void);


json_t *
get_parameters(const uint8_t fetch_all)
{
  json_t *params = json_object();

  plugin_parameters_add_double(params, BPP_VOLUME_SCALE, volume_scale, 0.1, 10, 0.1, "Volume scale");
  plugin_parameters_add_int(params, BPP_NB_SPHERES, nb_spheres, 1, 64, 1, "Number of spheres");
  plugin_parameters_add_double(params, BPP_RADIUS_FACTOR, radius_factor, 0, 0.24, 0.01, "Radius factor");
  plugin_parameters_add_double(params, BPP_SENSITIVITY, sensitivity, 0, 10, 0.1, "Sensitivity");
  plugin_parameters_add_double(params, BPP_MOVE_FACTOR, move_factor, 0, 10, 0.1, "Movement factor");
  plugin_parameters_add_double(params, BPP_BORDER_X, border_x, 0, 1, 0.01, "X margin");
  plugin_parameters_add_double(params, BPP_BORDER_Y, border_y, 0, 1, 0.01, "Y margin");

  return params;
}


void
set_parameters(const Context_t *ctx, const json_t *in_parameters)
{
  int reload = 0;

  reload |= plugin_parameter_parse_int_range(in_parameters, BPP_NB_SPHERES, &nb_spheres) & PLUGIN_PARAMETER_CHANGED;
  reload |= plugin_parameter_parse_double_range(in_parameters, BPP_RADIUS_FACTOR, &radius_factor) & PLUGIN_PARAMETER_CHANGED;
  plugin_parameter_parse_double_range(in_parameters, BPP_VOLUME_SCALE, &volume_scale);
  plugin_parameter_parse_double_range(in_parameters, BPP_SENSITIVITY, &sensitivity);
  plugin_parameter_parse_double_range(in_parameters, BPP_MOVE_FACTOR, &move_factor);
  plugin_parameter_parse_double_range(in_parameters, BPP_BORDER_X, &border_x);
  plugin_parameter_parse_double_range(in_parameters, BPP_BORDER_Y, &border_y);

  if (reload) {
    if (!xpthread_mutex_lock(&mutex)) {
      move_spheres();
      xpthread_mutex_unlock(&mutex);
    }
  }
}


json_t *
parameters(const Context_t *ctx, const json_t *in_parameters, const uint8_t fetch_all)
{
  if (NULL != in_parameters) {
    set_parameters(ctx, in_parameters);
  }

  return get_parameters(fetch_all);
}


inline static void
init_sphere(void)
{
  free_spheres();
  centres = (POSITION *)xcalloc(nb_spheres, sizeof(POSITION));

  /* max radius for this screen size */
  radius_max = (uint16_t)(radius_factor * HEIGHT);
  /* buffer size */
  size_max = (radius_max << 1) + 1;
  /* Allocate buffer */
  size_t n = (size_t) size_max * size_max;
  color_indices = (Pixel_t *) xcalloc(n, sizeof(Pixel_t));
}


inline static void
free_spheres(void)
{
  xfree(color_indices);
  xfree(centres);
}


inline static void
compute_radius(Context_t *ctx)
{
  float volume = volume_scale * Input_get_volume(ctx->input);

  /** Volume ajustement **/
  /* pow(volume, X) modify plugin sensitivity:
   * as volume is between 0.0 and 1.0,
   * X > 0 AND X < 1: "increase" the effect (more sensitive on small volumes),
   * X > 1: "decrease" the effect (less sensitive on small volumes and better on beats)
   */
  /* and "* 50.0" to reajust global volume, otherwise spheres are too small */
  volume = powf(volume, sensitivity) * 50.0;

  /* sphere radius */
  radius = (uint16_t)(volume * radius_max);
  radius = MIN(radius, radius_max);
}


/* compute color indices */
inline static void
compute_index(void)
{
  if (radius > 0) {
    Pixel_t *p = color_indices;
    float a, b;
    short dx, dy, r1;

    r1 = radius - 1;
    for (dy = -r1; dy <= r1; dy++) {
      b = (float)dy / radius;
      b *= b;
      for (dx = -r1; dx <= r1; dx++) {
        a = (float)dx / radius;
        a *= a;
        a += b;
        a = floor(sqrtf(1 - a) * 255);
        if (a > 255) {
          a = 255;
        } else if (a < 0) {
          a = 0;
        }
        *p++ = (Pixel_t) a;
      }
    }
  }
}


/* Move spheres randomly */
inline static void
move_spheres(void)
{
  init_sphere();

  uint16_t *p = (uint16_t *)centres;
  for (uint16_t i = 0; i < nb_spheres; i++) {
    *p++ = 2*radius_max + (uint16_t)(b_rand_int() % (WIDTH - 4*radius_max));
    *p++ = 2*radius_max + (uint16_t)(b_rand_int() % (HEIGHT - 4*radius_max));
  }
}


/* Plot one sphere */
inline static void
plot_sphere(Buffer8_t *dst, POSITION *pos)
{
  if (radius > 0) {
    Pixel_t *p = color_indices;

    uint16_t r1 = radius - 1;
    for (int16_t dy = -r1; dy <= r1; dy++) {
      uint16_t y = (pos->y + HEIGHT + dy) % HEIGHT;
      for (int16_t dx = -r1; dx <= r1; dx++) {
        Pixel_t index = *p++;
        if (index > 0) {
          uint16_t x = (pos->x + WIDTH + dx) % WIDTH;
          if (index > get_pixel_nc(dst, x, y)) {
            set_pixel_nc(dst, x, y, index);
          }
        }
      }
    }
  }
}


/* Plot all spheres */
inline static void
plot_spheres(Context_t *ctx)
{
  uint16_t length, offset, i, n, *p;
  Buffer8_t *dst = passive_buffer(ctx);

  Buffer8_clear(dst);

  /* max sphere move: +/- (radius/4) */
  offset = radius * move_factor / 4;
  length = offset * 2 + 1;

  uint16_t border_offset_x = MAX(2 * radius_max, border_x * HWIDTH);
  uint16_t border_offset_y = MAX(2 * radius_max, border_y * HHEIGHT);

  p = (uint16_t *)centres;
  for (i = 0; i < nb_spheres; i++) {
    plot_sphere(dst, (POSITION *)p);

    /* Move sphere */
    n = *p;
    *p++ = MAX( border_offset_x, MIN( (uint16_t) ((n + WIDTH  + (b_rand_int() % length) - offset) % WIDTH), WIDTH - border_offset_x));
    n = *p;
    *p++ = MAX( border_offset_y, MIN( (uint16_t) ((n + HEIGHT + (b_rand_int() % length) - offset) % HEIGHT), HEIGHT - border_offset_y));
  }
}


int8_t
create(Context_t *ctx)
{
  xpthread_mutex_init(&mutex, NULL);
  move_spheres();

  return 1;
}


void
run(Context_t *ctx)
{
  if (!xpthread_mutex_lock(&mutex)) {
    compute_radius(ctx);
    compute_index();
    plot_spheres(ctx);
    xpthread_mutex_unlock(&mutex);
  }
}


void
on_switch_on(Context_t *ctx)
{
  /* Initialize parameters */
  nb_spheres = 16;
  radius_factor = 0.04;
  volume_scale = 1;
  sensitivity = 3;
  move_factor = 1;
  border_x = 0;
  border_y = 0;

  move_spheres(); // mutex is not locked
}


void
destroy(Context_t *ctx)
{
  free_spheres();
  xpthread_mutex_destroy(&mutex);
}
