/* Water filter
 *
 * (c) Copyright 2000-2025 Denis Roio <jaromil@dyne.org>
 *
 * from an original idea of water algorithm by Federico 'Pix' Feroldi
 *
 * this code contains optimizations by Jason Hood and Scott Scriven
 *
 * animated background, 32bit colorspace and interactivity by jaromil
 * ported to C++ and frei0r plugin API in 2007
 *
 * This source code is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Public License as published
 * by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 *
 * This source code 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.
 * Please refer to the GNU Public License for more details.
 *
 * You should have received a copy of the GNU Public License along with
 * this source code; if not, write to:
 * Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#include <frei0r.hpp>

#define CLIP_EDGES \
  if(x - radius < 1) left -= (x-radius-1); \
  if(y - radius < 1) top  -= (y-radius-1); \
  if(x + radius > geo->w-1) right -= (x+radius-geo->w+1); \
  if(y + radius > geo->h-1) bottom-= (y+radius-geo->h+1);

/* water physics */
#define WATER 1
#define JELLY 2
#define SLUDGE 3
#define SUPER_SLUDGE 4

/* precalculated sinusoidal tables */
#include <math.h>
#define FSINMAX 2047
#define SINFIX 16
#define FSINBITS 16
#ifndef PI
#define PI 3.14159265358979323846
#endif

typedef struct {
  int16_t w;
  int16_t h;
  uint8_t bpp;
  uint32_t size;
} ScreenGeometry;

class Water: public frei0r::filter {
public:

  f0r_param_double physics;
  f0r_param_bool swirl;
  f0r_param_bool rain;
  f0r_param_bool surfer;
  f0r_param_bool smooth;
  f0r_param_bool distort;
  f0r_param_position position;
  //bool randomize_swirl;

  Water(unsigned int width, unsigned int height) {
    physics = 1.0;
    swirl = 1;
    rain = 0;
    surfer = 0;
    distort = 0;
    smooth = 0;
    position.x = 0.0;
    position.y = 0.0;
    //randomize_swirl = false;
    register_param(physics, "physics", "water density: from 0.0 to 1.0");
    register_param(swirl, "swirl", "swirling whirpool in the center");
    register_param(rain, "rain", "rain drops all over");
    register_param(surfer, "surfer", "surf the surface with a wandering finger");
    register_param(smooth, "smooth", "smooth up all perturbations on the surface");
    register_param(distort, "distort", "distort all surface like dropping a bucket to the floor");
    register_param(position, "position", "swirl position coordinate, Relative center coordinate");
    //register_param(randomize_swirl, "randomize_swirl", "randomize the swirling angle");

    Hpage = 0;
    ox = 80;
    oy = 80;
    done = 0;
    mode = 0x4000;

    BkGdImagePre = BkGdImage = BkGdImagePost = 0;
    Height[0] = Height[1] = 0;

    /* default physics */
    density = 4;
    pheight = 600;
    radius = 30;

    raincount = 0;
    blend = 0;

    fastsrand(::time(NULL));

    FCreateSines();

    geo = new ScreenGeometry();
    geo->w = width;
    geo->h = height;
    geo->size =  width*(height+1)*sizeof(uint32_t);

    water_surfacesize = geo->size;
    calc_optimization = (height)*(width);

    xang = fastrand()%2048;
    yang = fastrand()%2048;
    swirlangle = fastrand()%2048;

    /* buffer allocation tango */
    if ( width*height > 0 ) {
        Height[0] = (uint32_t*)calloc(width*(height+1), sizeof(uint32_t));
        Height[1] = (uint32_t*)calloc(width*(height+1), sizeof(uint32_t));
    }
    if ( geo->size > 0 ) {
        BkGdImagePre = (uint32_t*) malloc(geo->size);
        BkGdImage =    (uint32_t*) malloc(geo->size);
        BkGdImagePost = (uint32_t*)malloc(geo->size);
    }

    // Initialize surface to NULL
    surface = NULL;
  }

  ~Water() {
    delete geo;
    free(Height[0]);
    free(Height[1]);
    free(BkGdImagePre);
    free(BkGdImage);
    free(BkGdImagePost);
  }

  virtual void update(double time,
                        uint32_t* out,
                        const uint32_t* in) {
    memcpy(BkGdImage, in, width*height*sizeof(uint32_t));
    water_update(out);
  }

private:
  ScreenGeometry *geo;

  /* 2 pages of Height field */
  uint32_t *Height[2];
  /* 3 copies of the background */
  uint32_t *BkGdImagePre;
  uint32_t *BkGdImage;
  uint32_t *BkGdImagePost;

  //  uint32_t *buffer;

  void *surface;

  /* water effect variables */
  int Hpage;
  int xang, yang;
  int swirlangle;
  int x, y, ox, oy;
  int done;
  int mode;

  /* precalculated to optimize a bit */
  int water_surfacesize;
  int calc_optimization;

  /* density: water density (step 1)
     pheight: splash height (step 40)
     radius: waterdrop radius (step 1) */
  int density, pheight, radius;
  int offset;

  int raincount;
  int blend;

  void water_clear();
  void water_distort();
  void water_setphysics(double physics);
  void water_update(uint32_t *out);
  void water_drop(int x, int y);
  void water_bigsplash(int x, int y);
  void water_surfer();
  void water_swirl();
  void water_3swirls();

  void DrawWater(int page, uint32_t* out);
  void CalcWater(int npage, int density);
  void CalcWaterBigFilter(int npage, int density);

  void SmoothWater(int npage);

  void HeightBlob(int x, int y, int radius, int height, int page);
  void HeightBox (int x, int y, int radius, int height, int page);

  void WarpBlob(int x, int y, int radius, int height, int page);
  void SineBlob(int x, int y, int radius, int height, int page);

  /* precalculated sinusoidal tables */
  int FSinTab[2048], FCosTab[2048];
  int FSin(int angle) { return FSinTab[angle&FSINMAX]; }
  int FCos(int angle) { return FCosTab[angle&FSINMAX]; }
  void FCreateSines() {
    int i; double angle;
    for(i=0; i<2048; i++) {
      angle = (float)i * (PI/1024.0);
      FSinTab[i] = (int)(sin(angle) * (float)0x10000);
      FCosTab[i] = (int)(cos(angle) * (float)0x10000);
    }
  }

  /* cheap & fast randomizer (by Fukuchi Kentarou) */
  uint32_t randval;
  uint32_t fastrand() { return (randval=randval*1103515245+12345); };
  void fastsrand(uint32_t seed) { randval = seed; };

  /* integer optimized square root by jaromil */
  int isqrt(unsigned int x) {
    unsigned int m, y, b; m = 0x40000000;
    y = 0; while(m != 0) { b = y | m; y = y>>1;
      if(x>=b) { x=x-b; y=y|m; }
      m=m>>2; } return y;
  }

};

void Water::water_clear() {
  memset(Height[0], 0, water_surfacesize);
  memset(Height[1], 0, water_surfacesize);
}

void Water::water_distort() {
  memset(Height[Hpage], 0, water_surfacesize);
}

void Water::water_setphysics(double physics) {
  if(physics<0.25) {
    // case WATER:
    mode |= 0x4000;
    density=4;
    pheight=600;
  } else if(physics<0.50) {
    // case JELLY:
    mode &= 0xBFFF;
    density=3;
    pheight=400;
  } else if(physics<0.75) {
    // case SLUDGE:
    mode &= 0xBFFF;
    density=6;
    pheight=400;
  } else {
    //  case SUPER_SLUDGE:
    mode &=0xBFFF;
    density=8;
    pheight=400;
  }
}

void Water::water_update(uint32_t* out) {
  if(distort && !rain) water_distort();
  if(smooth) SmoothWater(Hpage);

  if(rain) {
    raincount++;
    if(raincount>3) {
      water_drop( (fastrand()%geo->w-40)+20 , (fastrand()%geo->h-40)+20 );
      raincount=0;
    }
  }

  if(swirl) water_swirl();
  if(surfer) water_surfer();
  DrawWater(Hpage,out);

  CalcWater(Hpage^1, density);
  Hpage ^=1 ;
}

void Water::water_drop(int x, int y) {
  if(mode & 0x4000)
    HeightBlob(x,y, radius>>2, pheight, Hpage);
  else
    WarpBlob(x, y, radius, pheight, Hpage);
}

void Water::water_bigsplash(int x, int y) {
  if(mode & 0x4000)
    HeightBlob(x, y, (radius>>1), pheight, Hpage);
  else
    SineBlob(x, y, radius, -pheight*6, Hpage);
}

void Water::water_surfer() {
  int x, y;
  x = (geo->w>>1)
    + ((
    (
     (FSin( (xang* 65) >>8) >>8) *
     (FSin( (xang*349) >>8) >>8)
     ) * ((geo->w-8)>>1)
    ) >> 16);
  y = (geo->h>>1)
    + ((
    (
     (FSin( (yang*377) >>8) >>8) *
     (FSin( (yang* 84) >>8) >>8)
     ) * ((geo->h-8)>>1)
    ) >> 16);
  xang += 13;
  yang += 12;

  if(mode & 0x4000)
    {
      offset = (oy+y)/2*geo->w + ((ox+x)>>1); // QUAAA
      Height[Hpage][offset] = pheight;
      Height[Hpage][offset + 1] =
      Height[Hpage][offset - 1] =
      Height[Hpage][offset + geo->w] =
      Height[Hpage][offset - geo->w] = pheight >> 1;

      offset = y*geo->w + x;
      Height[Hpage][offset] = pheight<<1;
      Height[Hpage][offset + 1] =
      Height[Hpage][offset - 1] =
      Height[Hpage][offset + geo->w] =
      Height[Hpage][offset - geo->w] = pheight;
    }
  else
    {
      SineBlob(((ox+x)>>1), ((oy+y)>>1), 3, -1200, Hpage);
      SineBlob(x, y, 4, -2000, Hpage);
    }

  ox = x;
  oy = y;
}

void Water::water_swirl() {
  int x, y;
  x = (geo->w>>1)
    + ((
    (FCos(swirlangle)) * (25)
    ) >> 16);

  y = (geo->h>>1)
    + ((
    (FSin(swirlangle)) * (25)
    ) >> 16);
  x += (int)(position.x * geo->w);
  y += (int)(position.y * geo->h);

  swirlangle += 50;
  if(mode & 0x4000)
    HeightBlob(x, y, radius>>2, pheight, Hpage);
  else
    WarpBlob(x, y, radius, pheight, Hpage);
}

void Water::water_3swirls() {
#define ANGLE 15
  int x, y;
  x = (95)
    + ((
    (FCos(swirlangle)) * (ANGLE)
    ) >> 16);
  y = (45)
    + ((
    (FSin(swirlangle)) * (ANGLE)
    ) >> 16);

  if(mode & 0x4000) HeightBlob(x,y, radius>>2, pheight, Hpage);
  else WarpBlob(x, y, radius, pheight, Hpage);

  x = (95)
    + ((
    (FCos(swirlangle)) * (ANGLE)
    ) >> 16);
  y = (255)
    + ((
    (FSin(swirlangle)) * (ANGLE)
    ) >> 16);

  if(mode & 0x4000) HeightBlob(x,y, radius>>2, pheight, Hpage);
  else WarpBlob(x, y, radius, pheight, Hpage);

  x = (345)
    + ((
    (FCos(swirlangle)) * (ANGLE)
    ) >> 16);
  y = (150)
    + ((
    (FSin(swirlangle)) * (ANGLE)
    ) >> 16);

 if(mode & 0x4000) HeightBlob(x,y, radius>>2, pheight, Hpage);
  else WarpBlob(x, y, radius, pheight, Hpage);

  swirlangle += 50;
}

/* internal physics routines */
void Water::DrawWater(int page,uint32_t* out) {
  int dx, dy;
  int offset=geo->w + 1;
  int newoffset;
  int maxoffset = geo->w * geo->h;
  int *ptr = (int*)&Height[page][0];

  for (int y = calc_optimization; offset < y; offset += 2) {
    for (int x = offset+geo->w-2; offset < x; offset++) {
      dx = ptr[offset] - ptr[offset+1];
      dy = ptr[offset] - ptr[offset+geo->w];
      newoffset = offset + geo->w*(dy>>3) + (dx>>3);
      if (newoffset >= 0 && newoffset < maxoffset) {
        out[offset] = BkGdImage[newoffset];
      }

      offset++;
      dx = ptr[offset] - ptr[offset+1];
      dy = ptr[offset] - ptr[offset+geo->w];
      newoffset = offset + geo->w*(dy>>3) + (dx>>3);
      if (newoffset >= 0 && newoffset < maxoffset) {
        out[offset] = BkGdImage[newoffset];
      }
    }
  }
}

void Water::CalcWater(int npage, int density) {
  int newh;
  int count = geo->w + 1;
  int *newptr = (int*) &Height[npage][0];
  int *oldptr = (int*) &Height[npage^1][0];

  for (int y = calc_optimization; count < y; count += 2) {
    for (int x = count+geo->w-2; count < x; count++) {
      /* eight pixels */
      newh = ((oldptr[count + geo->w]
           + oldptr[count - geo->w]
           + oldptr[count + 1]
           + oldptr[count - 1]
           + oldptr[count - geo->w - 1]
           + oldptr[count - geo->w + 1]
           + oldptr[count + geo->w - 1]
           + oldptr[count + geo->w + 1]
           ) >> 2 )
    - newptr[count];
      newptr[count] =  newh - (newh >> density);
    }
  }
}

void Water::SmoothWater(int npage) {
  int newh;
  int count = geo->w + 1;
  int *newptr = (int*) &Height[npage][0];
  int *oldptr = (int*) &Height[npage^1][0];

  for(int y=1; y<geo->h-1; y++) {
    for(int x=1; x<geo->w-1; x++) {
      /* eight pixel */
      newh          = ((oldptr[count + geo->w]
            + oldptr[count - geo->w]
            + oldptr[count + 1]
            + oldptr[count - 1]
            + oldptr[count - geo->w - 1]
            + oldptr[count - geo->w + 1]
            + oldptr[count + geo->w - 1]
            + oldptr[count + geo->w + 1]
            ) >> 3 )
    + newptr[count];


      newptr[count] =  newh>>1;
      count++;
    }
    count += 2;
  }
}

void Water::CalcWaterBigFilter(int npage, int density) {
  int newh;
  int count = (geo->w<<1) + 2;
  int *newptr = (int*) &Height[npage][0];
  int *oldptr = (int*) &Height[npage^1][0];

  for(int y=2; y<geo->h-2; y++) {
    for(int x=2; x<geo->w-2; x++) {
      /* 25 pixels */
      newh = (
          (
           (
        (oldptr[count + geo->w]
         + oldptr[count - geo->w]
         + oldptr[count + 1]
         + oldptr[count - 1]
         )<<1)
           + ((oldptr[count - geo->w - 1]
           + oldptr[count - geo->w + 1]
           + oldptr[count + geo->w - 1]
           + oldptr[count + geo->w + 1]))
           + ( (
            oldptr[count - (geo->w<<1)]
            + oldptr[count + (geo->w<<1)]
            + oldptr[count - 2]
            + oldptr[count + 2]
            ) >> 1 )
           + ( (
            oldptr[count - (geo->w<<1) - 1]
            + oldptr[count - (geo->w<<1) + 1]
            + oldptr[count + (geo->w<<1) - 1]
            + oldptr[count + (geo->w<<1) + 1]
            + oldptr[count - 2 - geo->w]
            + oldptr[count - 2 + geo->w]
            + oldptr[count + 2 - geo->w]
            + oldptr[count + 2 + geo->w]
            ) >> 2 )
           )
          >> 3)
    - (newptr[count]);
      newptr[count] =  newh - (newh >> density);
      count++;
    }
    count += 4;
  }
}

void Water::HeightBlob(int x, int y, int radius, int height, int page) {
  int rquad;
  int cx, cy, cyq;
  int left, top, right, bottom;

  rquad = radius * radius;

  /* Make a randomly-placed blob... */
  if(x<0) x = 1+radius+ fastrand()%(geo->w-2*radius-1);
  if(y<0) y = 1+radius+ fastrand()%(geo->h-2*radius-1);

  left=-radius; right = radius;
  top=-radius; bottom = radius;

  CLIP_EDGES

  for(cy = top; cy < bottom; cy++) {
    cyq = cy*cy;
    for(cx = left; cx < right; cx++) {
      if(cx*cx + cyq < rquad) {
        int index = geo->w*(cy+y) + (cx+x);
        // Bounds check
        if (index >= 0 && index < geo->w * geo->h) {
          Height[page][index] += height;
        }
      }
    }
  }
}


void Water::HeightBox (int x, int y, int radius, int height, int page) {
  int cx, cy;
  int left, top, right, bottom;

  if(x<0) x = 1+radius+ fastrand()%(geo->w-2*radius-1);
  if(y<0) y = 1+radius+ fastrand()%(geo->h-2*radius-1);

  left=-radius; right = radius;
  top=-radius; bottom = radius;

  CLIP_EDGES

  for(cy = top; cy < bottom; cy++) {
    for(cx = left; cx < right; cx++) {
      int index = geo->w*(cy+y) + (cx+x);
      // Bounds check
      if (index >= 0 && index < geo->w * geo->h) {
        Height[page][index] = height;
      }
    }
  }
}

void Water::WarpBlob(int x, int y, int radius, int height, int page) {
  int cx, cy;
  int left,top,right,bottom;
  int square;
  int radsquare = radius * radius;

  radsquare = (radius*radius);

  height = height>>5;

  left=-radius; right = radius;
  top=-radius; bottom = radius;

  CLIP_EDGES

  for(cy = top; cy < bottom; cy++) {
    for(cx = left; cx < right; cx++) {
      square = cy*cy + cx*cx;
      if(square < radsquare) {
        int index = geo->w*(cy+y) + cx+x;
        // Bounds check
        if (index >= 0 && index < geo->w * geo->h) {
          Height[page][index] += (int)((radius-isqrt(square))*(float)(height));
        }
      }
    }
  }
}

void Water::SineBlob(int x, int y, int radius, int height, int page) {
  int cx, cy;
  int left,top,right,bottom;
  int square, dist;
  int radsquare = radius * radius;
  float length = (1024.0/(float)radius)*(1024.0/(float)radius);

  if(x<0) x = 1+radius+ fastrand()%(geo->w-2*radius-1);
  if(y<0) y = 1+radius+ fastrand()%(geo->h-2*radius-1);

  radsquare = (radius*radius);
  left=-radius; right = radius;
  top=-radius; bottom = radius;

  CLIP_EDGES

  for(cy = top; cy < bottom; cy++) {
    for(cx = left; cx < right; cx++) {
      square = cy*cy + cx*cx;
      if(square < radsquare) {
        dist = (int)(isqrt(square*length));
        int index = geo->w*(cy+y) + cx+x;
        // Bounds check
        if (index >= 0 && index < geo->w * geo->h) {
          Height[page][index] += (int)((FCos(dist)+0xffff)*(height)) >> 19;
        }
      }
    }
  }
}

frei0r::construct<Water> plugin("Water",
                "water drops on a video surface",
                "Jaromil",
                4,0);
