/*
 *  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"


uint32_t version = 0;
uint32_t options = BO_LENS | BO_NOT_LENS;
char dname[] = "Taquin";
char desc[] = "Taquin effect";


static uint8_t size;

static Shuffler_t *shf = NULL;

#define MIN_SIZE 20
#define MAX_SIZE 200
#define LIMIT_SIZE MIN(MINSCREEN/2+1, MAX_SIZE)

#define TAQUIN_MIN -10;
#define NX (WIDTH / size)
#define NY (HEIGHT / size)

enum Dirs { SLIDE_RL, SLIDE_DT, SLIDE_LR, SLIDE_TD };

static uint16_t hole_x, hole_y, new_hole_x=0, new_hole_y=0;
static short   step;
static enum Dirs    dir;
static uint32_t  *map = NULL;

// #define TAQUIN_ORIGINAL // Define to get invalid writes
// #define TAQUIN_ASSERT   // Define to crash with assertions


static inline int8_t
is_ok(const uint16_t size)
{
  return ((!(WIDTH % size)) && (!(HEIGHT % size)));
  /* printf("[i] taquin: size= %d for %dx%d: %d, %d -> %d\n",
     size, WIDTH, HEIGHT, WIDTH%size, HEIGHT%size, res); */
}


static void
init_grid_2(void)
{
  uint16_t x, y, dx, dy;

  for (y = 0; y < NY; y++) {
    for (x = 0; x < NX; x++) {
      for (dy = 1; dy < size - 1; dy++) {
        for (dx = 1; dx < size - 1; dx++) {
          map[(y * size + 1) * WIDTH + x * size + dx] =
            map [(y * size + dy) * WIDTH + x * size + 1] =
              map [(y * size + size - 1) * WIDTH + x * size + dx] =
                map [(y * size + dy) * WIDTH + x * size + size - 2] =
                  1;
        }
      }
    }
  }
}


static void
init_hole(void)
{
  uint16_t i, j;

  for (j = 0; j < size; j++) {
    for (i = 0; i < size; i++) {
      map[(hole_y + j) * WIDTH + hole_x + i] = 0;
    }
  }
}


static void
new_hole(void)
{
  static short flip = 1;

  new_hole_x = hole_x;
  new_hole_y = hole_y;

  if (flip == 1) { /* horizontal move */
    while (new_hole_x == hole_x) {
      new_hole_x = (uint16_t)(drand48() * NX) * size;
    }
    dir = (new_hole_x > hole_x) ? SLIDE_RL : SLIDE_LR;
  } else { /* vertical move */
    while (new_hole_y == hole_y) {
      new_hole_y = (uint16_t)(drand48() * NY) * size;
    }
    dir = (new_hole_y > hole_y) ? SLIDE_DT : SLIDE_TD;
  }

  flip = -flip;
}


static void
init_grid(void)
{
  uint16_t gx, gy, i, j;

  for (gy = 0; gy < NY; gy++) {
    for (i = 0; i < WIDTH; i++) {
      map[(gy * size) * WIDTH + i] = 0;
      if (gy) {
        map[((gy * size) - 1) * WIDTH + i] = 0;
      }
    }
  }

  for (gx = 0; gx < NX; gx++) {
    for (j = 0; j < HEIGHT; j++) {
      map[j * WIDTH + (gx * size)] = 0;
      if (gx) {
        map[j * WIDTH + (gx * size) - 1] = 0;
      }
    }
  }

  init_grid_2();
  init_hole();
}


static inline int8_t
prepare(Shuffler_t *shf)
{
  int i;

  /* disable too small sizes */
  for (i = 0; i < MIN_SIZE; i++) {
    Shuffler_disable(shf, i);
  }

  /* check other sizes are valid */
  for (i = MIN_SIZE; i < LIMIT_SIZE; i++) {
    if (!is_ok(i)) {
      Shuffler_disable(shf, i);
    }
  }

  /* disable too big sizes */
  for (i = LIMIT_SIZE; i < MAX_SIZE; i++) {
    Shuffler_disable(shf, i);
  }

  return Shuffler_ok(shf);
}


int8_t
create(Context_t *ctx)
{
  if ((WIDTH < 3 * MIN_SIZE) || (HEIGHT < 3 * MIN_SIZE)) {
    printf("[!] taquin: not enough space\n");
    return 0;
  } else {
    shf = Shuffler_new(MAX_SIZE);
    /* Shuffler_verbose(shf); */
    if (prepare(shf)) {
      map = xcalloc(BUFFSIZE, sizeof(uint32_t));
      return 1;
    } else {
      return 0;
    }
  }
}


void
destroy(Context_t *ctx)
{
  if (NULL != shf) {
    Shuffler_delete(shf);
    xfree(map);
  }
}


static inline void
slide_rl_1(uint16_t from, uint16_t to, uint16_t y)
{
  uint16_t x;

  for (x = from; x < to; x++) {
    map[(y * WIDTH) + x] = map[(y * WIDTH) + x + 1];
  }
  map[(y * WIDTH) + to] = 0;
}


static inline void
slide_dt_1(uint16_t from, uint16_t to, uint16_t x)
{
  uint16_t y;

  for (y = from; y < to; y++) {
    map[(y * WIDTH) + x] = map[((y + 1) * WIDTH) + x];
  }
  map[(to * WIDTH) + x] = 0;
}


static inline void
slide_lr_1(uint16_t from, uint16_t to, uint16_t y)
{
  uint16_t x;

  // XXX FIXME Code below does invalid writes
#ifdef TAQUIN_ORIGINAL
  for (x = to; x > from; --x) {
    map[(y * WIDTH) + x] = map[(y * WIDTH) + x - 1];
  }
  map[(y * WIDTH) + from] = 0;
#else
  // XXX FIXME Temporary workaround
  for (x = to; x > from; --x) {
    long dst = (y * WIDTH) + x;
    long src = (y * WIDTH) + x - 1;
#ifdef TAQUIN_ASSERT
    printf("size: %d, BUFFSIZE: %ld, dst: %ld, src: %ld\n", size, BUFFSIZE, dst, src);
    assert(dst >= 0);
    assert(dst < (long)BUFFSIZE);
#else
    if ((dst >= 0) && (dst < (long)BUFFSIZE))
#endif
    map[dst] = map[src];
  }
  long null = (y * WIDTH) + from;
#ifdef TAQUIN_ASSERT
  assert(null >= 0);
  assert(null < (long)BUFFSIZE);
#endif
  map[null] = 0;
#endif
}


static inline void
slide_td_1(uint16_t from, uint16_t to, uint16_t x)
{
  uint16_t y;

  // XXX FIXME Code below does invalid writes
#ifdef TAQUIN_ORIGINAL
  for (y = to; y > from; --y) {
    map[(y * WIDTH) + x] = map[((y - 1) * WIDTH) + x];
  }
  map[(from * WIDTH) + x] = 0;
#else
  // XXX FIXME Temporary workaround
  for (y = to; y > from; --y) {
    long dst = (y * WIDTH) + x;
    long src = ((y - 1) * WIDTH) + x;
#ifdef TAQUIN_ASSERT
    printf("size: %d, BUFFSIZE: %ld, dst: %ld, src: %ld\n", size, BUFFSIZE, dst, src);
    assert(dst >= 0);
    assert(dst < (long)BUFFSIZE);
#else
    if ((dst >= 0) && (dst < (long)BUFFSIZE))
#endif
    map[dst] = map[src];
  }
  long null = (from * WIDTH) + x;
#ifdef TAQUIN_ASSERT
  assert(null >= 0);
  assert(null < (long)BUFFSIZE);
#endif
  map[null] = 0;
#endif
}


static inline void
slide_rl(void)
{
  uint16_t y;

  for (y = 0; y < size; y++) {
    slide_rl_1(hole_x, new_hole_x + size - step, hole_y + y);
  }
}


static inline void
slide_lr(void)
{
  uint16_t y;

  for (y = 0; y < size; y++) {
    slide_lr_1(new_hole_x - 1 + step, hole_x + size, hole_y + y);
  }
}


static inline void
slide_dt(void)
{
  uint16_t x;

  for (x = 0; x < size; x++) {
    slide_dt_1(hole_y, new_hole_y + size - step, hole_x + x);
  }
}


static inline void
slide_td(void)
{
  uint16_t x;

  for (x = 0; x < size; x++) {
    slide_td_1(new_hole_y - 1 + step, hole_y + size, hole_x + x);
  }
}


static inline void
slide(void)
{
  switch (dir) {
    case SLIDE_RL:
      slide_rl();
      break;
    case SLIDE_DT:
      slide_dt();
      break;
    case SLIDE_LR:
      slide_lr();
      break;
    case SLIDE_TD:
      slide_td();
      break;
    default:
      xerror("T'as qu'un taquin");
      break;
  }
}


void
run(Context_t *ctx)
{
  uint32_t k;
  Buffer8_t *src0 = active_buffer(ctx);
  const Buffer8_t *src = active_buffer(ctx);
  Buffer8_t *dst = passive_buffer(ctx);

  if (step < 0) {
    ++step;
  } else {
    if (++step == (size + 1)) {
      hole_x = new_hole_x;
      hole_y = new_hole_y;
      new_hole();
      step = TAQUIN_MIN;
    } else {
      slide();
    }
  }

  set_pixel(src0, 0, 0, 0);
  set_pixel(src0, 1, 0, 200);

  for (k = 0; k < BUFFSIZE; k++) {
    assert(map[k] < BUFFSIZE);
    dst->buffer[k] = src->buffer[map[k]];
  }
}


void
on_switch_on(Context_t *ctx)
{
  uint32_t k;

  for (k = 0; k < BUFFSIZE; k++) {
    map[k] = k;
  }

  size = Shuffler_get(shf);

  hole_x = (uint16_t)(drand48() * NX) * size;
  hole_y = (uint16_t)(drand48() * NY) * size;
  new_hole();

  init_grid();
  step = TAQUIN_MIN;
}
