/*
 *  Copyright 1994-2022 Olivier Girondel
 *
 *  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 "context.h"
#include "spline.h"
#include "particles.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 dname[] = "Takens";
char desc[] = "Stereo phase-space reconstruction with spline and particles";


// default delay for phase-space reconstructions
#define DEFAULT_PHASE_SPACE_DELAY 10

// default spline span size
#define DEFAULT_SPAN_SIZE 6

// to separate left and right channels
#define DEFAULT_XOFFSET 0.5

/* Parameters */
static double volume_scale = 1;
static int delay = DEFAULT_PHASE_SPACE_DELAY;
static int do_connect = 1;
static double pos_factor = 0;
static double vel_factor = 0;
static double ttl_factor = 0;
static int do_particles = 0;
static int stereo = 0;
static double x_offset = DEFAULT_XOFFSET;
static int span_size = DEFAULT_SPAN_SIZE;
static int use_aspect_ratio = 0;


static Particle_System_t *ps = NULL;
static Spline_t *s[2] = { NULL, NULL };
static pthread_mutex_t mutex;


static uint32_t
get_phase_space_samples(const Context_t *ctx, const uint8_t delay)
{
  if (NULL != ctx->input) {
    return ctx->input->size - 2 * delay;
  } else {
    return 0;
  }
}


static void
alloc_spline(const Context_t *ctx, Spline_t **s, const uint8_t delay, const uint8_t span_size)
{
  uint32_t samples = get_phase_space_samples(ctx, delay);

  Spline_delete(*s);
#ifdef DEBUG
  VERBOSE(printf("[i] (re)allocating spline (delay: %d, span: %d, samples: %d)\n", delay, span_size, samples));
#endif
  *s = Spline_new(span_size, samples);
  Spline_info(*s);
}


static void
delay_spline(Context_t *ctx, Spline_t *s, const enum Channel channel, const float x_offset)
{
  Input_t *input = ctx->input;
  uint32_t x = 0;
  uint32_t y = delay;
  uint32_t z = 2 * delay;

  if (use_aspect_ratio) {
    float y_scale = (float)HEIGHT / (float)WIDTH;
    for ( ; z < input->size; x++, y++, z++) {
      s->cpoints[x].pos.x = volume_scale * input->data[channel][x] + x_offset;
      s->cpoints[x].pos.y = volume_scale * input->data[channel][y] * y_scale + x_offset;
      s->cpoints[x].pos.z = volume_scale * input->data[channel][z] + x_offset;
    }
  } else
    for ( ; z < input->size; x++, y++, z++) {
      s->cpoints[x].pos.x = volume_scale * input->data[channel][x] + x_offset;
      s->cpoints[x].pos.y = volume_scale * input->data[channel][y] + x_offset;
      s->cpoints[x].pos.z = volume_scale * input->data[channel][z] + x_offset;
    }
  Spline_compute(s);
}


/* Parameters */
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_boolean(params, BPP_CONNECT, do_connect, "Draw with lines");
  plugin_parameters_add_int(params, BPP_SPAN_SIZE, span_size, 0, 20, 1, "Number of intermediary points");
  plugin_parameters_add_boolean(params, BPP_PARTICLES, do_particles, "Use particles");
  if (fetch_all || do_particles) {
    plugin_parameters_add_double(params, BPP_POS_FACTOR, pos_factor, 1, 100, 0.01, "Position factor");
    plugin_parameters_add_double(params, BPP_VEL_FACTOR, vel_factor, 1, 100, 0.01, "Velocity factor");
    plugin_parameters_add_double(params, BPP_TTL_FACTOR, ttl_factor, 1, 100, 0.01, "Time to live factor");
  }
  plugin_parameters_add_boolean(params, BPP_STEREO, stereo, "Separate channels");
  if (fetch_all || stereo) {
    plugin_parameters_add_double(params, BPP_XOFFSET, x_offset, 0.01, 10, 0.01, "Distance");
  }
  plugin_parameters_add_boolean(params, BPP_USE_ASPECT_RATIO, use_aspect_ratio, "Use aspect ratio");

  return params;
}


void
set_parameters(const Context_t *ctx, const json_t *in_parameters)
{
  plugin_parameter_parse_double_range(in_parameters, BPP_VOLUME_SCALE, &volume_scale);

  int realloc_splines = 0;
  realloc_splines |= plugin_parameter_parse_int_range(in_parameters, BPP_DELAY, &delay) & PLUGIN_PARAMETER_CHANGED;

  // v1 API compat
  int channels = stereo ? 2 : 1;
  plugin_parameter_parse_int_range(in_parameters, BPP_CONNECT, &do_connect);
  realloc_splines |= plugin_parameter_parse_int_range(in_parameters, BPP_CHANNELS, &channels) & PLUGIN_PARAMETER_CHANGED;
  plugin_parameter_parse_int_range(in_parameters, BPP_PARTICLES, &do_particles);
  plugin_parameter_parse_int_range(in_parameters, BPP_USE_ASPECT_RATIO, &use_aspect_ratio);

  // v2 API
  plugin_parameter_parse_boolean(in_parameters, BPP_CONNECT, &do_connect);
  realloc_splines |= plugin_parameter_parse_boolean(in_parameters, BPP_STEREO, &stereo) & PLUGIN_PARAMETER_CHANGED;
  plugin_parameter_parse_boolean(in_parameters, BPP_PARTICLES, &do_particles);
  plugin_parameter_parse_boolean(in_parameters, BPP_USE_ASPECT_RATIO, &use_aspect_ratio);

  if (realloc_splines) {
    if (!stereo) {
      x_offset = 0;
    } else {
      x_offset = DEFAULT_XOFFSET;
    }
  }
  plugin_parameter_parse_double_range(in_parameters, BPP_XOFFSET, &x_offset);

  realloc_splines |= plugin_parameter_parse_int_range(in_parameters, BPP_SPAN_SIZE, &span_size) & PLUGIN_PARAMETER_CHANGED;

  if (span_size && realloc_splines) {
    if (!xpthread_mutex_lock(&mutex)) {
      alloc_spline(ctx, &s[0], delay, span_size);
      alloc_spline(ctx, &s[1], delay, span_size);
      xpthread_mutex_unlock(&mutex);
    }
  }

  plugin_parameter_parse_double_range(in_parameters, BPP_POS_FACTOR, &pos_factor);
  plugin_parameter_parse_double_range(in_parameters, BPP_VEL_FACTOR, &vel_factor);
  plugin_parameter_parse_double_range(in_parameters, BPP_TTL_FACTOR, &ttl_factor);
}


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);
}


/* Plugin callbacks */
int8_t
create(Context_t *ctx)
{
  alloc_spline(ctx, &s[0], delay, span_size);
  alloc_spline(ctx, &s[1], delay, span_size);
  ps = Particle_System_new(PS_NOLIMIT);
  xpthread_mutex_init(&mutex, NULL);

  return 1;
}


void
on_switch_on(Context_t *ctx)
{
  /* Initialize parameters */
  volume_scale = 1;
  pos_factor = 2.5;
  vel_factor = 0.1;
  ttl_factor = 1.0;
  use_aspect_ratio = 0;
}


void
destroy(Context_t *ctx)
{
  Spline_delete(s[0]);
  Spline_delete(s[1]);
  Particle_System_delete(ps);
  xpthread_mutex_destroy(&mutex);
}


static void
delay_draw_spline(Context_t *ctx)
{
  Buffer8_t *dst = passive_buffer(ctx);
  const Params3d_t *params3d = &ctx->params3d;
  Input_t *input = ctx->input;
  int channels = stereo + 1;

  Buffer8_clear(dst);

  for (uint8_t c = 0; c < channels; c++) {
    int64_t points = s[c]->nb_spoints - 1;
    if (!do_connect) {
      points++;
    }

    for (int64_t i = 0; i < points; i++) {
      Pixel_t color = Input_random_color(input);
      if (do_connect) {
        draw_line_3d(params3d, dst, &s[c]->spoints[i], &s[c]->spoints[i + 1], color);
      } else {
        set_pixel_3d(params3d, dst, &s[c]->spoints[i], color);
      }
    }
  }
}


static void
delay_particles_spline(Context_t *ctx)
{
  Input_t *input = ctx->input;
  Buffer8_t *dst = passive_buffer(ctx);
  const Params3d_t *params3d = &ctx->params3d;
  int channels = stereo + 1;

  Particle_System_go(ps);

  for (uint8_t c = 0; c < channels; c++) {
    Point3d_t origin;
    origin.pos.x = (channels == 1) ? 0.0 : ((c == 0) ? -x_offset : x_offset);
    origin.pos.y = origin.pos.z = 0.0;

    for (int64_t i = 0; (i < s[c]->nb_spoints) && Particle_System_can_add(ps); i++) {
      const float ttl = Input_random_float_range(input, 1.5, 2.5) * ttl_factor;
      const Pixel_t col = Input_random_color(input);
      const Particle_t *part = Particle_new_indexed(ttl, col, p3d_mul(&s[c]->spoints[i], pos_factor), p3d_mul(&s[c]->spoints[i], vel_factor), origin, 0.0);

      Particle_System_add(ps, part);
    }
  }

  Particle_System_draw(ps, params3d, dst);
}


void
run(Context_t *ctx)
{
  if (!xpthread_mutex_lock(&mutex)) {
    if (!stereo) {
      delay_spline(ctx, s[0], A_MONO, 0.0);
    } else {
      delay_spline(ctx, s[0], A_LEFT,  -x_offset);
      delay_spline(ctx, s[1], A_RIGHT, +x_offset);
    }
    delay_draw_spline(ctx);

    if (do_particles) {
      delay_particles_spline(ctx);
    }
    xpthread_mutex_unlock(&mutex);
  }
}
