/*
 *  Copyright 1994-2022 Olivier Girondel
 *  Copyright 2019-2022 Tavasti
 *
 *  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/>.
 */

/*
 * Idea: Drops of paint on picture.
 */

#include "context.h"

#define DROPCOUNT        30  /* how many drops there can be same time progressing */
#define INITIAL_WAIT_MAX 5   /* max wait before first drop */
#define WAIT_MIN         1   /* minimum wait between drops */
#define WAIT_MAX         11  /* max wait between drops */
#define DROPSIZE_MIN     (WIDTH/30.0) /* minimum radius of drop */
#define DROPSIZE_MAX     (WIDTH/17.0) /* maximum radius of drop */
#define HAZY_RADIUS      3.0  /* how many pixels on outer ring are hazy */
#define HAZY_RATIO       10.0 /* 1/x probability hazy pixel hazy to get drawn */
#define PROG_RATIO       0.21 /* how big ratio of diameter progress in one round */
#define READY_TOL        0.3  /* difference to target regarded as ready */


uint32_t version = 0;
uint32_t options = BO_GFX;
char desc[] = "Color drops";
char dname[] = "Paint drops";

typedef struct Drop_s {
  int x;
  int y;
  Pixel_t color;  /* target color */
  double target;  /* target radius */
  double current; /* current radius */
} Drop_t;

static Drop_t drops[DROPCOUNT];
static short *mask = NULL;
static int next_drop; /* counter how many rounds before next drop */


int8_t
create(Context_t *ctx)
{
  mask = xmalloc(sizeof(short) * BUFFSIZE);

  return 1;
}


void
destroy(Context_t *ctx)
{
  xfree(mask);
}


void
on_switch_on(Context_t *ctx)
{
  uint32_t i;
  /* Mark all drops as free */
  for (i = 0; i < DROPCOUNT ; i++) {
    drops[i].x = -1;
  }
  next_drop = b_rand_uint32_range(0, INITIAL_WAIT_MAX);
  /* initiallize mask */
  for (i = 0; i < BUFFSIZE ; i++) {
    mask[i] = -1;
  }
}


/* helper function for marking affected pixels of circle to mask */
static void
mark_circle_range(int x, int y, double r, short mark, short *mbuff)
{
  assert(NULL != mask);

  /* calculate bounding box of circle */
  int left = x - r;
  left = (left < 0) ? 0 : left;
  int right = x + r;
  right = (right >= WIDTH) ? WIDTH - 1 : right;
  int top = y - r;
  top = (top < 0) ? 0 : top;
  int bottom = y + r;
  bottom = (bottom >= HEIGHT) ? HEIGHT - 1 : bottom;

  /* we handle distances as squared to speed up things,
     no need to calculate sqrt for every pixel */
  double distsq = r * r; /* outer ring which is hazy*/
  double distsq2 = (r - HAZY_RADIUS) * (r - HAZY_RADIUS); /* inner ring */
  int i,j;
  /* loop thru bounding box, and check if pixel is in circle */
  for (i = top; i <= bottom; i++) {
    for (j = left ; j <= right; j++) {
      int dx = j - x;
      int dy = i - y;
      if (((dx * dx) + (dy * dy)) < distsq) {
        if (((dx * dx) + (dy * dy)) < distsq2) {
          mbuff[i * WIDTH + j] = mark;
        } else if (b_rand_uint32_range(0, HAZY_RATIO) == 0) {
          /* hazy pixels */
          mbuff[i * WIDTH + j] = mark;
        } else {
          /* outside circle */
          mbuff[i * WIDTH + j] = -1;
        }
      }
    }
  }
}


void
run(Context_t *ctx)
{
  uint32_t i;
  /* Grow existing drops, and release fully grown drops for next use */
  for (i = 0; i < DROPCOUNT; i++) {
    if (drops[i].x > -1) { /* in use */
      if ((drops[i].current + READY_TOL) < drops[i].target) {
        drops[i].current += (drops[i].target - drops[i].current) * PROG_RATIO;
        /* mark area where this drop affects this round */
        mark_circle_range(drops[i].x, drops[i].y, drops[i].current, i, mask);
      } else {
        /* this drop is done */
        mark_circle_range(drops[i].x, drops[i].y,drops[i].current, -1, mask);
        drops[i].x = -1;
      }
    }
  }

  /* start checking if we need and can add new drop */
  if (--next_drop <= 0) {
    /* time to add drop if there is room in struct */
    for (i = 0; i < DROPCOUNT; i++) {
      if (drops[i].x == -1) { /* free entry */
        /* new random drop */
        drops[i].x = b_rand_uint32_range(0, WIDTH);
        drops[i].y = b_rand_uint32_range(0, HEIGHT);
        drops[i].color = b_rand_uint32_range(0, PIXEL_MAXVAL);
        drops[i].target = b_rand_double_range(DROPSIZE_MIN, DROPSIZE_MAX);
        drops[i].current = drops[i].target * PROG_RATIO ;
        next_drop = b_rand_uint32_range(WAIT_MIN, WAIT_MAX);
        /* mark area where this drop affects this round */
        mark_circle_range(drops[i].x, drops[i].y, drops[i].current, i, mask);
        break;
      }
    }
  }
  /* actual picture handling */
  const Pixel_t *src = active_buffer(ctx)->buffer;
  Pixel_t *dst = passive_buffer(ctx)->buffer;

  for (i = 0; i < BUFFSIZE; i++) {
    Pixel_t col = src[i];
    if (mask[i] >= 0) {
      col = drops[mask[i]].color;
    }
    *dst++ = col;
  }
}
