/*
 *  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 acid dropping to picture. Drop will grow, and eat
 * colors away. Some drops are alcalic, giving COLOR_MAXVAL instead of 0
 */

#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         2   /* minimum wait between drops */
#define WAIT_MAX         15  /* max wait between drops */
#define DROPSIZE_MIN     (WIDTH/17.0) /* minimum radius of drop */
#define DROPSIZE_MAX     (WIDTH/7.0)  /* maximum radius of drop */
#define HAZY_RADIUS      20.0 /* how many pixels on outer ring are hazy */
#define HAZY_RATIO       23.0 /* 1/x probability hazy pixel to get drawn */
#define PROG_RATIO       0.11 /* how big ratio of diameter progress in one round */
#define INCR_RATIO       0.39 /* Ratio how much color value is changed */
#define READY_TOL        0.06 /* difference to target regarded as ready */
#define ACID_ALCALIC_RATIO 4  /* how much more acid than alcalic */

uint32_t version = 0;
uint32_t options = BO_GFX | BO_SCHEMES;
char desc[] = "Color-corroding drops";
char dname[] = "Acid drops";
char html_description[] = "<p>Drops of acid dropping to picture. A drop will grow and eat \
colors away. Some drops are alkalic, giving maximum color instead of minimum.</p>";

typedef struct Drop_s {
  int x;
  int y;
  Pixel_t color;  /* target color */
  double target;  /* target radius */
  double current; /* current radius */
  int incr;       /* color increment / decrement value */
} 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)
{
  /* clear drops */
  for (uint32_t i = 0; i < DROPCOUNT ; i++) {
    drops[i].x = -1;
  }
  next_drop = b_rand_uint32_range(0, INITIAL_WAIT_MAX);
  /* initialize mask */
  for (uint32_t 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 */

  /* loop thru bounding box, and check if pixel is in circle */
  for (int i = top; i <= bottom; i++) {
    for (int 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)
{
  /* Grow existing drops, and release fully grown drops for next use */
  for (uint32_t 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;
        drops[i].incr = (drops[i].target - drops[i].current) * INCR_RATIO;
        if (drops[i].incr < 1.0) {
          drops[i].incr = 1.0; /* values less that 1 won't have any effect */
        }
        /* 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 (uint8_t 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);
        /* more acid drops than alcalic */
        drops[i].color = (b_rand_uint32_range(0, ACID_ALCALIC_RATIO) > 1) ? 0 : PIXEL_MAXVAL;
        drops[i].target = b_rand_double_range(DROPSIZE_MIN, DROPSIZE_MAX);
        drops[i].current = drops[i].target * PROG_RATIO ;
        drops[i].incr = drops[i].target;
        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 (uint32_t i = 0; i < BUFFSIZE; i++) {
    Pixel_t col = src[i];
    if (mask[i] >= 0) {
      if (col > drops[mask[i]].color) {
        col = (col > drops[mask[i]].incr) ? col - drops[mask[i]].incr : drops[mask[i]].color;
      } else if (col < drops[mask[i]].color) {
        col = (col < (drops[mask[i]].color - drops[mask[i]].incr)) ? col + drops[mask[i]].incr : drops[mask[i]].color;
      }
    }
    *dst++ = col;
  }
}
