/*
 * Copyright (c) 2010 Jice
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * The name of Jice may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY JICE ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL JICE BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
#include "main.hpp"

#include <math.h>
#include <stdio.h>

// gas simulation
// based on Jos Stam, "Real-Time Fluid Dynamics for Games". Proceedings of the Game Developer Conference, March 2003.
// http://www.dgp.toronto.edu/people/stam/reality/Research/pub.html

#define WIDTH 50
#define HEIGHT 50

#define WIDTHx2 (WIDTH * 2)
#define HEIGHTx2 (HEIGHT * 2)
// use square map
#define N MIN(WIDTHx2, HEIGHTx2)
// store a 2D map in a 1D array
#define SIZE (N + 2) * (N + 2)
// convert x,y to array index
#define IX(i, j) ((i) + (N + 2) * (j))
#define SWAP(x0, x)  \
  {                  \
    float* tmp = x0; \
    x0 = x;          \
    x = tmp;         \
  }

// 2D velocity maps (current and previous)
float u[SIZE], v[SIZE], u_prev[SIZE], v_prev[SIZE];
// density maps (current and previous)
float dens[SIZE], dens_prev[SIZE];
TCODImage img(WIDTHx2, HEIGHTx2);

float visc = 1E-6f;
float diff = 1E-5f;
float force = 40.0f;
float source = 5000.0f;
float stepDelay = 0.0f;

int player_x = N / 4, player_y = N / 4;

// set boundary conditions
void set_bnd(int b, float* x) {
  for (int i = 1; i <= N; i++) {
    // west and east walls
    x[IX(0, i)] = b == 1 ? -x[IX(1, i)] : x[IX(1, i)];
    x[IX(N + 1, i)] = b == 1 ? -x[IX(N, i)] : x[IX(N, i)];
    // boundary doesn't work on north and south walls...
    // dunno why...
    x[IX(i, 0)] = b == 1 ? -x[IX(i, 1)] : x[IX(i, 1)];
    x[IX(i, N + 1)] = b == 1 ? -x[IX(i, N)] : x[IX(i, N)];
  }
  // boundary conditions at corners
  x[IX(0, 0)] = 0.5f * (x[IX(1, 0)] + x[IX(0, 1)]);
  x[IX(0, N + 1)] = 0.5f * (x[IX(1, N + 1)] + x[IX(0, N)]);
  x[IX(N + 1, 0)] = 0.5f * (x[IX(N, 0)] + x[IX(N + 1, 1)]);
  x[IX(N + 1, N + 1)] = 0.5f * (x[IX(N, N + 1)] + x[IX(N + 1, N)]);
}

// update density map according to density sources
// x : density map
// s : density source map
// dt : elapsed time
void add_source(float* x, float* s, float dt) {
  for (int i = 0; i < SIZE; i++) {
    x[i] += dt * s[i];
  }
}

// update density or velocity map for diffusion
// b : boundary width
// x : current density map
// x0 : previous density map
// diff : diffusion coef
// dt : elapsed time
void diffuse(int b, float* x, float* x0, float diff, float dt) {
  float a = diff * dt * N * N;
  for (int k = 0; k < 20; k++) {
    for (int i = 1; i <= N; i++) {
      for (int j = 1; j <= N; j++) {
        x[IX(i, j)] =
            (x0[IX(i, j)] + a * (x[IX(i - 1, j)] + x[IX(i + 1, j)] + x[IX(i, j - 1)] + x[IX(i, j + 1)])) / (1 + 4 * a);
      }
    }
    set_bnd(b, x);
  }
}

// update density map according to velocity map
// b : boundary width
// d : current density map
// d0 : previous density map
// u,v : current velocity map
// dt : elapsed time
void advect(int b, float* d, float* d0, float* u, float* v, float dt) {
  int i0, j0, i1, j1;
  float x, y, s0, t0, s1, t1, dt0;

  dt0 = dt * N;
  for (int i = 1; i <= N; i++) {
    for (int j = 1; j <= N; j++) {
      x = i - dt0 * u[IX(i, j)];
      y = j - dt0 * v[IX(i, j)];
      if (x < 0.5) x = 0.5;
      if (x > N + 0.5) x = N + 0.5;
      i0 = (int)x;
      i1 = i0 + 1;
      if (y < 0.5) y = 0.5;
      if (y > N + 0.5) y = N + 0.5;
      j0 = (int)y;
      j1 = j0 + 1;
      s1 = x - i0;
      s0 = 1 - s1;
      t1 = y - j0;
      t0 = 1 - t1;
      d[IX(i, j)] = s0 * (t0 * d0[IX(i0, j0)] + t1 * d0[IX(i0, j1)]) + s1 * (t0 * d0[IX(i1, j0)] + t1 * d0[IX(i1, j1)]);
    }
  }
  set_bnd(b, d);
}

void project(float* u, float* v, float* p, float* div) {
  float h = 1.0f / N;
  for (int i = 1; i <= N; i++) {
    for (int j = 1; j <= N; j++) {
      div[IX(i, j)] = -0.5f * h * (u[IX(i + 1, j)] - u[IX(i - 1, j)] + v[IX(i, j + 1)] - v[IX(i, j - 1)]);
      p[IX(i, j)] = 0;
    }
  }
  set_bnd(0, div);
  set_bnd(0, p);

  for (int k = 0; k < 20; k++) {
    for (int i = 1; i <= N; i++) {
      for (int j = 1; j <= N; j++) {
        p[IX(i, j)] = (div[IX(i, j)] + p[IX(i - 1, j)] + p[IX(i + 1, j)] + p[IX(i, j - 1)] + p[IX(i, j + 1)]) / 4;
      }
    }
    set_bnd(0, p);
  }

  for (int i = 1; i <= N; i++) {
    for (int j = 1; j <= N; j++) {
      u[IX(i, j)] -= 0.5f * (p[IX(i + 1, j)] - p[IX(i - 1, j)]) / h;
      v[IX(i, j)] -= 0.5f * (p[IX(i, j + 1)] - p[IX(i, j - 1)]) / h;
    }
  }
  set_bnd(1, u);
  set_bnd(2, v);
}

// do all three density steps
void update_density(float* x, float* x0, float* u, float* v, float diff, float dt) {
  add_source(x, x0, dt);
  SWAP(x0, x);
  diffuse(0, x, x0, diff, dt);
  SWAP(x0, x);
  advect(0, x, x0, u, v, dt);
}

void update_velocity(float* u, float* v, float* u0, float* v0, float visc, float dt) {
  add_source(u, u0, dt);
  add_source(v, v0, dt);
  SWAP(u0, u);
  diffuse(1, u, u0, visc, dt);
  SWAP(v0, v);
  diffuse(2, v, v0, visc, dt);
  project(u, v, u0, v0);
  SWAP(u0, u);
  SWAP(v0, v);
  advect(1, u, u0, u0, v0, dt);
  advect(2, v, v0, u0, v0, dt);
  project(u, v, u0, v0);
}

void init() {
  memset(u, 0, sizeof(float) * SIZE);
  memset(v, 0, sizeof(float) * SIZE);
  memset(u_prev, 0, sizeof(float) * SIZE);
  memset(v_prev, 0, sizeof(float) * SIZE);
  for (int i = 0; i < SIZE; i++) dens[i] = 0.0f;
  memcpy(dens_prev, dens, sizeof(float) * SIZE);
}

void get_from_UI(float* d, float* u, float* v, float elapsed, TCOD_key_t k, TCOD_mouse_t mouse) {
  int i, j;
  float vx = 0.0f, vy = 0.0f;

  stepDelay -= elapsed;
  if (stepDelay < 0.0f) {
    if (TCODConsole::isKeyPressed(TCODK_UP) && player_y > 0) {
      player_y--;
      vy -= force;
    }
    if (TCODConsole::isKeyPressed(TCODK_DOWN) && player_y < N / 2 - 1) {
      player_y++;
      vx += force;
    }
    if (TCODConsole::isKeyPressed(TCODK_LEFT) && player_x > 0) {
      player_x--;
      vx -= force;
    }
    if (TCODConsole::isKeyPressed(TCODK_RIGHT) && player_x < N / 2 - 1) {
      player_x++;
      vx += force;
    }
    stepDelay = 0.2f;  // move 5 cells per second
    // try to move smoke when you walk inside it. doesn't seem to work...
    u[IX(player_x * 2, player_y * 2)] = 5 * vx;
    v[IX(player_x * 2, player_y * 2)] = 5 * vy;
  }

  for (i = 0; i < SIZE; i++) {
    u[i] = v[i] = d[i] = 0.0f;
  }

  if (!mouse.lbutton && !mouse.rbutton) return;

  i = mouse.cx * 2;
  j = mouse.cy * 2;
  if (i < 1 || i > N || j < 1 || j > N) return;

  if (mouse.lbutton) {
    float dx, dy, l;
    dx = (float)(mouse.cx - player_x);
    dy = (float)(mouse.cy - player_y);
    l = sqrtf(dx * dx + dy * dy);
    if (l > 0) {
      l = 1.0f / l;
      dx *= l;
      dy *= l;
      u[IX(player_x * 2, player_y * 2)] = force * dx;
      v[IX(player_x * 2, player_y * 2)] = force * dy;
      d[IX(player_x * 2, player_y * 2)] = source;
    }
  }
}

void update(float elapsed, TCOD_key_t k, TCOD_mouse_t mouse) {
  get_from_UI(dens_prev, u_prev, v_prev, elapsed, k, mouse);
  update_velocity(u, v, u_prev, v_prev, visc, elapsed);
  update_density(dens, dens_prev, u, v, diff, elapsed);
}

void render() {
  static TCODColor deepBlue = TCODColor::darkestFlame;
  static TCODColor highBlue = TCODColor::lightestYellow;
  for (int x = 0; x <= N; x++) {
    for (int y = 0; y <= N; y++) {
      float coef = (float)(dens[IX(x, y)] / 128.0f);
      coef = CLAMP(0.0f, 1.0f, coef);
      img.putPixel(x, y, TCODColor::lerp(deepBlue, highBlue, coef));
    }
  }
  img.blit2x(TCODConsole::root, 0, 0);
  TCODConsole::root->print(2, HEIGHT - 2, "%4d fps", TCODSystem::getFps());
  TCODConsole::root->setDefaultForeground(TCODColor::white);
  TCODConsole::root->putChar(player_x, player_y, '@');
}

int main(int argc, char* argv[]) {
  // initialize the game window
  TCODConsole::initRoot(WIDTH, HEIGHT, "pyromancer flame spell v" VERSION, false, TCOD_RENDERER_OPENGL2);
  TCODSystem::setFps(25);
  TCODMouse::showCursor(true);

  bool endCredits = false;
  init();

  while (!TCODConsole::isWindowClosed()) {
    TCOD_key_t k;
    TCOD_mouse_t mouse;

    TCODSystem::checkForEvent(TCOD_EVENT_KEY | TCOD_EVENT_MOUSE, &k, &mouse);
    /*
                    v_prev[IX(N/2,0)] = 1.0f;
                    u_prev[IX(N/3,N/3)]=1.0f;
                    dens_prev[IX(N/2,0)] = 128.0f;
    */
    if (k.vk == TCODK_PRINTSCREEN) {
      // screenshot
      if (!k.pressed) TCODSystem::saveScreenshot(NULL);
      k.vk = TCODK_NONE;
    } else if (k.lalt && (k.vk == TCODK_ENTER || k.vk == TCODK_KPENTER)) {
      // switch fullscreen
      if (!k.pressed) TCODConsole::setFullscreen(!TCODConsole::isFullscreen());
      k.vk = TCODK_NONE;
    }
    // update the game
    update(TCODSystem::getLastFrameLength(), k, mouse);

    // render the game screen
    render();
    TCODConsole::root->print(5, 49, "Arrows to move, left mouse button to cast");
    // render libtcod credits
    if (!endCredits) endCredits = TCODConsole::renderCredits(4, 4, true);
    // flush updates to screen
    TCODConsole::root->flush();
  }
  return 0;
}
