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

#include <libtcod/gui/gui.hpp>

#include "operation.hpp"

TCODHeightMap *hm = NULL, *hmold = NULL;
TCODNoise* noise = NULL;
TCODRandom *rnd = NULL, *backupRnd = NULL;
TCODConsole* guicon = NULL;
bool greyscale = false;
bool slope = false;
bool normal = false;
bool isNormalized = true;
bool oldNormalized = true;
char msg[512] = "";
float msgDelay = 0.0f;
float hillRadius = 0.1f;
float hillVariation = 0.5f;
float addFbmDelta = 0.0f;
float scaleFbmDelta = 0.0f;
uint32_t seed = 0xdeadbeef;

float sandHeight = 0.12f;
float grassHeight = 0.315f;
float snowHeight = 0.785f;

char landMassTxt[128] = "";
char minZTxt[128] = "";
char maxZTxt[128] = "";
char seedTxt[128] = "";
float mapmin = 0.0f, mapmax = 0.0f;
float oldmapmin = 0.0f, oldmapmax = 0.0f;

// ui
ToolBar* params = NULL;
ToolBar* history = NULL;
ToolBar* colorMapGui = NULL;

float voronoiCoef[2] = {-1.0f, 1.0f};

/* light 3x3 smoothing kernel :
        1  2 1
        2 20 2
        1  2 1
*/
int smoothKernelSize = 9;
int smoothKernelDx[9] = {-1, 0, 1, -1, 0, 1, -1, 0, 1};
int smoothKernelDy[9] = {-1, -1, -1, 0, 0, 0, 1, 1, 1};
float smoothKernelWeight[9] = {1, 2, 1, 2, 20, 2, 1, 2, 1};

TCODColor mapGradient[256];
#define MAX_COLOR_KEY 10

// TCOD's land color map
static int keyIndex[MAX_COLOR_KEY] = {
    0,
    (int)(sandHeight * 255),
    (int)(sandHeight * 255) + 4,
    (int)(grassHeight * 255),
    (int)(grassHeight * 255) + 10,
    (int)(snowHeight * 255),
    (int)(snowHeight * 255) + 10,
    255};
static TCODColor keyColor[MAX_COLOR_KEY] = {
    TCODColor(0, 0, 50),       // deep water
    TCODColor(30, 30, 170),    // water-sand transition
    TCODColor(114, 150, 71),   // sand
    TCODColor(80, 120, 10),    // sand-grass transition
    TCODColor(17, 109, 7),     // grass
    TCODColor(120, 220, 120),  // grass-snow transisiton
    TCODColor(208, 208, 239),  // snow
    TCODColor(255, 255, 255)};
static Image* keyImages[MAX_COLOR_KEY];

static int nbColorKeys = 8;

void initColors() { TCODColor::genMap(mapGradient, nbColorKeys, keyColor, keyIndex); }

void render() {
  static TCODHeightMap backup(HM_WIDTH, HM_HEIGHT);
  isNormalized = true;
  TCODConsole::root->setDefaultBackground(TCODColor::black);
  TCODConsole::root->setDefaultForeground(TCODColor::white);
  TCODConsole::root->clear();
  backup.copy(hm);
  mapmin = 1E8f;
  mapmax = -1E8f;
  for (int i = 0; i < HM_WIDTH * HM_HEIGHT; i++) {
    if (hm->values[i] < 0.0f || hm->values[i] > 1.0f) {
      isNormalized = false;
    }
    if (hm->values[i] < mapmin) mapmin = hm->values[i];
    if (hm->values[i] > mapmax) mapmax = hm->values[i];
  }
  if (!isNormalized) backup.normalize();
  // render the TCODHeightMap
  for (int x = 0; x < HM_WIDTH; x++) {
    for (int y = 0; y < HM_HEIGHT; y++) {
      float z = backup.getValue(x, y);
      uint8_t val = (uint8_t)(z * 255);
      if (slope) {
        // render the slope map
        z = CLAMP(0.0f, 1.0f, hm->getSlope(x, y) * 10.0f);
        val = (uint8_t)(z * 255);
        TCODColor c(val, val, val);
        TCODConsole::root->setCharBackground(x, y, c);
      } else if (greyscale) {
        // render the greyscale heightmap
        TCODColor c(val, val, val);
        TCODConsole::root->setCharBackground(x, y, c);
      } else if (normal) {
        // render the normal map
        float n[3];
        hm->getNormal((float)x, (float)y, n, mapmin);
        uint8_t r = (uint8_t)((n[0] * 0.5f + 0.5f) * 255);
        uint8_t g = (uint8_t)((n[1] * 0.5f + 0.5f) * 255);
        uint8_t b = (uint8_t)((n[2] * 0.5f + 0.5f) * 255);
        TCODColor c(r, g, b);
        TCODConsole::root->setCharBackground(x, y, c);
      } else {
        // render the colored heightmap
        TCODConsole::root->setCharBackground(x, y, mapGradient[val]);
      }
    }
  }
  sprintf(minZTxt, "min z    : %.2f", mapmin);
  sprintf(maxZTxt, "max z    : %.2f", mapmax);
  sprintf(seedTxt, "seed     : %X", seed);
  float landProportion = 100.0f - 100.0f * backup.countCells(0.0f, sandHeight) / (hm->w * hm->h);
  sprintf(landMassTxt, "landMass : %d %%", (int)landProportion);
  if (!isNormalized)
    TCODConsole::root->printf(HM_WIDTH / 2, HM_HEIGHT - 1, TCOD_BKGND_NONE, TCOD_CENTER, "the map is not normalized !");
  // message
  msgDelay -= TCODSystem::getLastFrameLength();
  if (msg[0] != 0 && msgDelay > 0.0f) {
    int h = TCODConsole::root->printRectEx(
        HM_WIDTH / 2, HM_HEIGHT / 2 + 1, HM_WIDTH / 2 - 2, 0, TCOD_BKGND_NONE, TCOD_CENTER, msg);
    TCODConsole::root->setDefaultBackground(TCODColor::lightBlue);
    if (h > 0) TCODConsole::root->rect(HM_WIDTH / 4, HM_HEIGHT / 2, HM_WIDTH / 2, h + 2, false, TCOD_BKGND_SET);
    TCODConsole::root->setDefaultBackground(TCODColor::black);
  }
}

void message(float delay, const char* fmt, ...) {
  va_list ap;
  msgDelay = delay;
  va_start(ap, fmt);
  vsprintf(msg, fmt, ap);
  va_end(ap);
}

void backup() {
  // save the heightmap & RNG states
  for (int x = 0; x < HM_WIDTH; x++) {
    for (int y = 0; y < HM_HEIGHT; y++) {
      hmold->setValue(x, y, hm->getValue(x, y));
    }
  }
  if (backupRnd) delete backupRnd;
  backupRnd = rnd->save();
  oldNormalized = isNormalized;
  oldmapmax = mapmax;
  oldmapmin = mapmin;
}

void restore() {
  // restore the previously saved heightmap & RNG states
  for (int x = 0; x < HM_WIDTH; x++) {
    for (int y = 0; y < HM_HEIGHT; y++) {
      hm->setValue(x, y, hmold->getValue(x, y));
    }
  }
  rnd->restore(backupRnd);
  isNormalized = oldNormalized;
  mapmax = oldmapmax;
  mapmin = oldmapmin;
}

void save() {
  // TODO
  message(2.0f, "Saved.");
}

void load() {
  // TODO
}

void addHill(int nbHill, float baseRadius, float radiusVar, float height) {
  for (int i = 0; i < nbHill; i++) {
    float hillMinRadius = baseRadius * (1.0f - radiusVar);
    float hillMaxRadius = baseRadius * (1.0f + radiusVar);
    float radius = rnd->getFloat(hillMinRadius, hillMaxRadius);
    float theta = rnd->getFloat(0.0f, 6.283185f);  // between 0 and 2Pi
    float dist = rnd->getFloat(0.0f, (float)MIN(HM_WIDTH, HM_HEIGHT) / 2 - radius);
    int xh = (int)(HM_WIDTH / 2 + cos(theta) * dist);
    int yh = (int)(HM_HEIGHT / 2 + sin(theta) * dist);
    hm->addHill((float)xh, (float)yh, radius, height);
  }
}

void clearCbk(Widget* w, void* data) {
  hm->clear();
  Operation::clear();
  history->clear();
  params->clear();
  params->setVisible(false);
}

void reseedCbk(Widget* w, void* data) {
  seed = rnd->getInt(0x7FFFFFFF, 0xFFFFFFFF);
  Operation::reseed();
  message(3.0f, "Switching to seed %X", seed);
}

void cancelCbk(Widget* w, void* data) { Operation::cancel(); }

// operations buttons callbacks
void normalizeCbk(Widget* w, void* data) { (new NormalizeOperation(0.0f, 1.0f))->add(); }

void addFbmCbk(Widget* w, void* data) { (new AddFbmOperation(1.0f, addFbmDelta, 0.0f, 6.0f, 1.0f, 0.5f))->add(); }

void scaleFbmCbk(Widget* w, void* data) { (new ScaleFbmOperation(1.0f, addFbmDelta, 0.0f, 6.0f, 1.0f, 0.5f))->add(); }

void addHillCbk(Widget* w, void* data) {
  (new AddHillOperation(25, 10.0f, 0.5f, (mapmax == mapmin ? 0.5f : (mapmax - mapmin) * 0.1f)))->add();
}

void rainErosionCbk(Widget* w, void* data) { (new RainErosionOperation(1000, 0.05f, 0.05f))->add(); }

void smoothCbk(Widget* w, void* data) { (new SmoothOperation(mapmin, mapmax, 2))->add(); }

void voronoiCbk(Widget* w, void* data) { (new VoronoiOperation(100, 2, voronoiCoef))->add(); }

void noiseLerpCbk(Widget* w, void* data) {
  float v = (mapmax - mapmin) * 0.5f;
  if (v == 0.0f) v = 1.0f;
  (new NoiseLerpOperation(0.0f, 1.0f, addFbmDelta, 0.0f, 6.0f, v, v))->add();
}

void raiseLowerCbk(Widget* w, void* data) { (new AddLevelOperation(0.0f))->add(); }

// In/Out buttons callbacks

void exportCCbk(Widget* w, void* data) {
  const char* code = Operation::buildCode(Operation::C);
  FILE* f = fopen("hm.c", "wt");
  fprintf(f, "%s", code);
  fclose(f);
  message(3.0f, "The code has been exported to ./hm.c");
}

void exportPyCbk(Widget* w, void* data) {
  const char* code = Operation::buildCode(Operation::PY);
  FILE* f = fopen("hm.py", "wt");
  fprintf(f, "%s", code);
  fclose(f);
  message(3.0f, "The code has been exported to ./hm.py");
}

void exportCppCbk(Widget* w, void* data) {
  const char* code = Operation::buildCode(Operation::CPP);
  FILE* f = fopen("hm.cpp", "wt");
  fprintf(f, "%s", code);
  fclose(f);
  message(3.0f, "The code has been exported to ./hm.cpp");
}

void exportBmpCbk(Widget* w, void* data) {
  TCODImage img(HM_WIDTH, HM_HEIGHT);
  for (int x = 0; x < HM_WIDTH; x++) {
    for (int y = 0; y < HM_HEIGHT; y++) {
      float z = hm->getValue(x, y);
      uint8_t val = (uint8_t)(z * 255);
      if (slope) {
        // render the slope map
        z = CLAMP(0.0f, 1.0f, hm->getSlope(x, y) * 10.0f);
        val = (uint8_t)(z * 255);
        TCODColor c(val, val, val);
        img.putPixel(x, y, c);
      } else if (greyscale) {
        // render the greyscale heightmap
        TCODColor c(val, val, val);
        img.putPixel(x, y, c);
      } else if (normal) {
        // render the normal map
        float n[3];
        hm->getNormal((float)x, (float)y, n, mapmin);
        uint8_t r = (uint8_t)((n[0] * 0.5f + 0.5f) * 255);
        uint8_t g = (uint8_t)((n[1] * 0.5f + 0.5f) * 255);
        uint8_t b = (uint8_t)((n[2] * 0.5f + 0.5f) * 255);
        TCODColor c(r, g, b);
        img.putPixel(x, y, c);
      } else {
        // render the colored heightmap
        img.putPixel(x, y, mapGradient[val]);
      }
    }
  }
  img.save("hm.bmp");
  message(3.0f, "The bitmap has been exported to ./hm.bmp");
}

// Display buttons callbacks
void colorMapCbk(Widget* w, void* data) {
  slope = false;
  greyscale = false;
  normal = false;
}

void greyscaleCbk(Widget* w, void* data) {
  slope = false;
  greyscale = true;
  normal = false;
}

void slopeCbk(Widget* w, void* data) {
  slope = true;
  greyscale = false;
  normal = false;
}

void normalCbk(Widget* w, void* data) {
  slope = false;
  greyscale = false;
  normal = true;
}

void changeColorMapIdxCbk(Widget* w, float val, void* data) {
  intptr_t i = ((intptr_t)data);
  keyIndex[i] = (int)(val);
  if (i == 1) sandHeight = (float)(i) / 255.0f;
  initColors();
}

void changeColorMapRedCbk(Widget* w, float val, void* data) {
  intptr_t i = ((intptr_t)data);
  keyColor[i].r = (int)(val);
  keyImages[i]->setBackgroundColor(keyColor[i]);
  initColors();
}

void changeColorMapGreenCbk(Widget* w, float val, void* data) {
  intptr_t i = ((intptr_t)data);
  keyColor[i].g = (int)(val);
  keyImages[i]->setBackgroundColor(keyColor[i]);
  initColors();
}

void changeColorMapBlueCbk(Widget* w, float val, void* data) {
  intptr_t i = ((intptr_t)data);
  keyColor[i].b = (int)(val);
  keyImages[i]->setBackgroundColor(keyColor[i]);
  initColors();
}

void changeColorMapEndCbk(Widget* w, void* data) { colorMapGui->setVisible(false); }

void changeColorMapCbk(Widget* w, void* data) {
  colorMapGui->move(w->x + w->w + 2, w->y);
  colorMapGui->clear();
  for (int i = 0; i < nbColorKeys; i++) {
    char tmp[64];
    sprintf(tmp, "Color %d", i);
    colorMapGui->addSeparator(tmp);
    HBox* hbox = new HBox(0, 0, 0);
    VBox* vbox = new VBox(0, 0, 0);
    colorMapGui->addWidget(hbox);
    Slider* idxSlider = new Slider(0, 0, 3, 0.0f, 255.0f, "index", "Index of the key in the color map (0-255)");
    idxSlider->setValue((float)keyIndex[i]);
    idxSlider->setFormat("%.0f");
    idxSlider->setCallback(changeColorMapIdxCbk, (void*)i);
    vbox->addWidget(idxSlider);
    keyImages[i] = new Image(0, 0, 0, 2);
    keyImages[i]->setBackgroundColor(keyColor[i]);
    vbox->addWidget(keyImages[i]);
    hbox->addWidget(vbox);

    vbox = new VBox(0, 0, 0);
    hbox->addWidget(vbox);
    Slider* redSlider = new Slider(0, 0, 3, 0.0f, 255.0f, "r", "Red component of the color");
    redSlider->setValue((float)keyColor[i].r);
    redSlider->setFormat("%.0f");
    redSlider->setCallback(changeColorMapRedCbk, (void*)i);
    vbox->addWidget(redSlider);
    Slider* greenSlider = new Slider(0, 0, 3, 0.0f, 255.0f, "g", "Green component of the color");
    greenSlider->setValue((float)keyColor[i].g);
    greenSlider->setFormat("%.0f");
    greenSlider->setCallback(changeColorMapGreenCbk, (void*)i);
    vbox->addWidget(greenSlider);
    Slider* blueSlider = new Slider(0, 0, 3, 0.0f, 255.0f, "b", "Blue component of the color");
    blueSlider->setValue((float)keyColor[i].b);
    blueSlider->setFormat("%.0f");
    blueSlider->setCallback(changeColorMapBlueCbk, (void*)i);
    vbox->addWidget(blueSlider);
  }
  colorMapGui->addWidget(new Button("Ok", NULL, changeColorMapEndCbk, NULL));
  colorMapGui->setVisible(true);
}

// build gui

void addOperationButton(ToolBar* tools, Operation::OpType type, void (*cbk)(Widget* w, void* data)) {
  tools->addWidget(new Button(Operation::names[type], Operation::tips[type], cbk, NULL));
}

void buildGui() {
  // status bar
  new StatusBar(0, 0, HM_WIDTH, 1);

  VBox* vbox = new VBox(0, 2, 1);
  // stats
  ToolBar* stats = new ToolBar(0, 0, 21, "Stats", "Statistics about the current map");
  stats->addWidget(new Label(0, 0, landMassTxt, "Ratio of land surface / total surface"));
  stats->addWidget(new Label(0, 0, minZTxt, "Minimum z value in the map"));
  stats->addWidget(new Label(0, 0, maxZTxt, "Maximum z value in the map"));
  stats->addWidget(new Label(0, 0, seedTxt, "Current random seed used to build the map"));
  vbox->addWidget(stats);

  // tools
  ToolBar* tools = new ToolBar(0, 0, 15, "Tools", "Tools to modify the heightmap");
  tools->addWidget(new Button("cancel", "Delete the selected operation", cancelCbk, NULL));
  tools->addWidget(new Button("clear", "Remove all operations and reset all heightmap values to 0.0", clearCbk, NULL));
  tools->addWidget(new Button("reseed", "Replay all operations with a new random seed", reseedCbk, NULL));

  // operations
  tools->addSeparator("Operations", "Apply a new operation to the map");
  addOperationButton(tools, Operation::NORM, normalizeCbk);
  addOperationButton(tools, Operation::ADDFBM, addFbmCbk);
  addOperationButton(tools, Operation::SCALEFBM, scaleFbmCbk);
  addOperationButton(tools, Operation::ADDHILL, addHillCbk);
  addOperationButton(tools, Operation::RAIN, rainErosionCbk);
  addOperationButton(tools, Operation::SMOOTH, smoothCbk);
  addOperationButton(tools, Operation::VORONOI, voronoiCbk);
  addOperationButton(tools, Operation::NOISELERP, noiseLerpCbk);
  addOperationButton(tools, Operation::ADDLEVEL, raiseLowerCbk);

  // display
  tools->addSeparator("Display", "Change the type of display");
  RadioButton::setDefaultGroup(1);
  RadioButton* colormap = new RadioButton("colormap", "Enable colormap mode", colorMapCbk, NULL);
  tools->addWidget(colormap);
  colormap->select();
  tools->addWidget(new RadioButton("slope", "Enable slope mode", slopeCbk, NULL));
  tools->addWidget(new RadioButton("greyscale", "Enable greyscale mode", greyscaleCbk, NULL));
  tools->addWidget(new RadioButton("normal", "Enable normal map mode", normalCbk, NULL));
  tools->addWidget(new Button("change colormap", "Modify the colormap used by hmtool", changeColorMapCbk, NULL));

  // change colormap gui
  colorMapGui = new ToolBar(0, 0, "Colormap", "Select the color and position of the keys in the color map");
  colorMapGui->setVisible(false);

  // in/out
  tools->addSeparator("In/Out", "Import/Export stuff");
  tools->addWidget(new Button("export C", "Generate the C code for this heightmap in ./hm.c", exportCCbk, NULL));
  tools->addWidget(
      new Button("export CPP", "Generate the CPP code for this heightmap in ./hm.cpp", exportCppCbk, NULL));
  tools->addWidget(
      new Button("export PY", "Generate the Python code for this heightmap in ./hm.py", exportPyCbk, NULL));
  tools->addWidget(new Button("export bmp", "Save this heightmap as a bitmap in ./hm.bmp", exportBmpCbk, NULL));

  vbox->addWidget(tools);

  // params box
  params = new ToolBar(0, 0, "Params", "Parameters of the current tool");
  vbox->addWidget(params);
  params->setVisible(false);

  // history
  history = new ToolBar(0, tools->y + 1 + tools->h, 15, "History", "History of operations");
  vbox->addWidget(history);
}

int main(int argc, char* argv[]) {
  TCODConsole::initRoot(HM_WIDTH, HM_HEIGHT, "height map tool", false, TCOD_RENDERER_OPENGL2);
  guicon = new TCODConsole(HM_WIDTH, HM_HEIGHT);
  guicon->setKeyColor(TCODColor(255, 0, 255));
  Widget::setConsole(guicon);
  TCODSystem::setFps(25);
  initColors();
  buildGui();
  hm = new TCODHeightMap(HM_WIDTH, HM_HEIGHT);
  hmold = new TCODHeightMap(HM_WIDTH, HM_HEIGHT);
  rnd = new TCODRandom(seed);
  noise = new TCODNoise(2, rnd);
  uint8_t fade = 50;
  bool creditsEnd = false;

  while (!TCODConsole::isWindowClosed()) {
    render();
    guicon->setDefaultBackground(TCODColor(255, 0, 255));
    guicon->clear();
    Widget::renderWidgets();
    if (!creditsEnd) {
      creditsEnd = TCODConsole::renderCredits(HM_WIDTH - 20, HM_HEIGHT - 7, true);
    }
    if (Widget::focus) {
      if (fade < 200) fade += 20;
    } else {
      if (fade > 80) fade -= 20;
    }
    TCODConsole::blit(guicon, 0, 0, HM_WIDTH, HM_HEIGHT, TCODConsole::root, 0, 0, fade / 255.0f, fade / 255.0f);
    TCODConsole::flush();
    TCOD_key_t key;
    TCOD_mouse_t mouse;
    TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS | TCOD_EVENT_MOUSE, &key, &mouse);
    Widget::updateWidgets(key, mouse);
    switch (key.c) {
      case '+':
        (new AddLevelOperation((mapmax - mapmin) / 50))->run();
        break;
      case '-':
        (new AddLevelOperation(-(mapmax - mapmin) / 50))->run();
        break;
      default:
        break;
    }
    switch (key.vk) {
      case TCODK_PRINTSCREEN:
        TCODSystem::saveScreenshot(NULL);
        break;
      default:
        break;
    }
  }

  return 0;
}
