/*
 * Photon reactor
 * Copyright (c) 2011 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 names 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 <libtcod.h>
#include <math.h>

#include "bsp_helper.hpp"
#include "rad_shader.hpp"

#define CON_WIDTH 80
#define CON_HEIGHT 50

#define MAP_WIDTH 39
#define MAP_HEIGHT 50

#define LIGHT_RADIUS 10
#define CELL_REFLECTIVITY 1.5f
#define CELL_SELF_ILLUMINATION 0.4f

TCODMap* map;
BspHelper bsp;
int player_x = 0, player_y = 0, playerBack;
Shader* leftShader = NULL;
Shader* rightShader = NULL;
TCODColor darkWall(50, 50, 150);
TCODColor lightWall(130, 110, 50);
TCODColor darkGround(0, 0, 100);
TCODColor lightGround(200, 180, 50);
int torchIndex;

float stdTime = 0.0f;
float radTime = 0.0f;
float stdLength = 0.0f;
float radLength = 0.0f;
int timeSecond;
int framesCount = 0;

// gamma correction lookup table
bool enableGammaCorrection = true;
int gammaLookup[256];
#define GAMMA (1 / 2.2f)

// find the closest walkable position
void findPos(int* x, int* y) {
  for (; (*x) < MAP_WIDTH; (*x)++) {
    for (; (*y) < MAP_HEIGHT; (*y)++) {
      if (map->isWalkable((*x), (*y))) return;
    }
  }
  for (*x = 0; (*x) < MAP_WIDTH; (*x)++) {
    for (*y = 0; (*y) < MAP_HEIGHT; (*y)++) {
      if (map->isWalkable((*x), (*y))) return;
    }
  }
}

void init() {
  TCODConsole::root->clear();

  // build the dungeon
  map = new TCODMap(MAP_WIDTH, MAP_HEIGHT);
  bsp.createBspDungeon(map, NULL);
  // empty map
  // map->clear(true,true);

  // create shaders
  leftShader = new StandardShader();
  rightShader = new PhotonShader(CELL_REFLECTIVITY, CELL_SELF_ILLUMINATION, 3);

  // put random lights
  for (int i = 0; i < 10; i++) {
    int lx = TCODRandom::getInstance()->getInt(1, MAP_WIDTH - 2);
    int ly = TCODRandom::getInstance()->getInt(1, MAP_HEIGHT - 2);
    findPos(&lx, &ly);
    leftShader->addLight(lx, ly, LIGHT_RADIUS, TCODColor::white);
    rightShader->addLight(lx, ly, LIGHT_RADIUS, TCODColor::white);
    TCODConsole::root->setChar(lx, ly, '*');
    TCODConsole::root->setChar(lx + CON_WIDTH / 2, ly, '*');
  }

  // find a starting position for the player
  findPos(&player_x, &player_y);
  playerBack = TCODConsole::root->getChar(player_x, player_y);
  TCODConsole::root->setChar(player_x, player_y, '@');
  TCODConsole::root->setChar(player_x + CON_WIDTH / 2, player_y, '@');

  // add the player's torch
  torchIndex = leftShader->addLight(player_x, player_y, 10, TCODColor::white);
  rightShader->addLight(player_x, player_y, LIGHT_RADIUS, TCODColor::white);

  // init shaders (must be done after adding lights for photon shader)
  leftShader->init(map);
  rightShader->init(map);

  timeSecond = TCODSystem::getElapsedMilli() / 1000;

  if (enableGammaCorrection) {
    for (int i = 0; i < 256; i++) {
      float v = i / 255.0f;
      float correctedV = powf(v, GAMMA);
      gammaLookup[i] = (int)(correctedV * 255);
    }
  } else {
    for (int i = 0; i < 256; i++) {
      gammaLookup[i] = i;
    }
  }
}

void render() {
  // compute lights
  framesCount++;
  uint32_t start = TCODSystem::getElapsedMilli();
  leftShader->compute();
  uint32_t leftEnd = TCODSystem::getElapsedMilli();
  rightShader->compute();
  uint32_t rightEnd = TCODSystem::getElapsedMilli();
  stdTime += (leftEnd - start) * 0.001f;
  radTime += (rightEnd - leftEnd) * 0.001f;
  if ((int)(start / 1000) != timeSecond) {
    timeSecond = start / 1000;
    stdLength = stdTime * 1000 / framesCount;
    radLength = radTime * 1000 / framesCount;
    stdTime = 0.0f;
    radTime = 0.0f;
    framesCount = 0;
  }

  for (int x = 0; x < MAP_WIDTH; x++) {
    for (int y = 0; y < MAP_HEIGHT; y++) {
      TCODColor darkCol, lightCol;
      // get the cell dark and lit colors
      if (map->isWalkable(x, y)) {
        darkCol = darkGround;
        lightCol = lightGround;
      } else {
        darkCol = darkWall;
        lightCol = lightWall;
      }
      // render left map
      // hack : for a better look, lights are white and we only use them as
      // a lerp coefficient between dark and light colors.
      // a true light model would multiply the light color with the cell color
      TCODColor leftLight = leftShader->getLightColor(x, y);
      TCODColor cellLeftCol = TCODColor::lerp(darkCol, lightCol, gammaLookup[leftLight.r] / 255.0f);
      TCODConsole::root->setCharBackground(x, y, cellLeftCol);

      // render right map
      TCODColor rightLight = rightShader->getLightColor(x, y);
      TCODColor cellRightCol = TCODColor::lerp(darkCol, lightCol, gammaLookup[rightLight.r] / 255.0f);
      TCODConsole::root->setCharBackground(x + CON_WIDTH / 2, y, cellRightCol);
    }
  }
  TCODConsole::root->print(CON_WIDTH / 4, 0, "Standard lighting %1.2fms", stdLength);
  TCODConsole::root->print(3 * CON_WIDTH / 4, 0, "Photon reactor %1.2fms", radLength);
}

void move(int dx, int dy) {
  if (map->isWalkable(player_x + dx, player_y + dy)) {
    // restore the previous map char
    TCODConsole::root->setChar(player_x, player_y, playerBack);
    TCODConsole::root->setChar(player_x + CON_WIDTH / 2, player_y, playerBack);
    // move the player
    player_x += dx;
    player_y += dy;
    playerBack = TCODConsole::root->getChar(player_x, player_y);
    // render the player
    TCODConsole::root->setChar(player_x, player_y, '@');
    TCODConsole::root->setChar(player_x + CON_WIDTH / 2, player_y, '@');
    // update the player's torch position
    leftShader->updateLight(torchIndex, player_x, player_y, LIGHT_RADIUS, TCODColor::white);
    rightShader->updateLight(torchIndex, player_x, player_y, LIGHT_RADIUS, TCODColor::white);
  }
}

int main() {
  TCODConsole::initRoot(80, 50, "Photon reactor - radiosity engine for roguelikes", false, TCOD_RENDERER_OPENGL2);
  TCODConsole::root->setAlignment(TCOD_CENTER);
  TCOD_key_t k = {TCODK_NONE, 0};
  TCOD_mouse_t mouse;

  init();
  while (!TCODConsole::isWindowClosed()) {
    render();
    TCODConsole::flush();
    TCODSystem::checkForEvent((TCOD_event_t)TCOD_EVENT_KEY_PRESS, &k, &mouse);
    switch (k.vk) {
      // move with arrows or numpad (2468)
      case TCODK_KP8:
      case TCODK_UP:
        move(0, -1);
        break;
      case TCODK_KP2:
      case TCODK_DOWN:
        move(0, 1);
        break;
      case TCODK_KP4:
      case TCODK_LEFT:
        move(-1, 0);
        break;
      case TCODK_KP6:
      case TCODK_RIGHT:
        move(1, 0);
        break;
      case TCODK_CHAR:
        switch (k.c) {
          // move with vi keys (HJKL) or FPS keys (WSAD or ZQSD)
          case 'q':
          case 'Q':
          case 'a':
          case 'A':
          case 'h':
          case 'H':
            move(-1, 0);
            break;
          case 's':
          case 'S':
          case 'j':
          case 'J':
            move(0, 1);
            break;
          case 'z':
          case 'Z':
          case 'w':
          case 'W':
          case 'k':
          case 'K':
            move(0, -1);
            break;
          case 'd':
          case 'D':
          case 'l':
          case 'L':
            move(1, 0);
            break;
          default:
            break;
        }
        break;
      case TCODK_PRINTSCREEN:
        TCODSystem::saveScreenshot(NULL);
        break;
      default:
        break;
    }
  }
  return 0;
}
