/*
 * Poly2Tri Copyright (c) 2009-2018, Poly2Tri Contributors
 * https://github.com/jhasse/poly2tri
 *
 * 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.
 * * Neither the name of Poly2Tri nor the names of its contributors may be
 *   used to endorse or promote products derived from this software without specific
 *   prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "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 THE COPYRIGHT OWNER OR
 * CONTRIBUTORS 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 <poly2tri/poly2tri.h>

#include <GLFW/glfw3.h>

#include <algorithm>
#include <cassert>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <fstream>
#include <iostream>
#include <iterator>
#include <list>
#include <numeric>
#include <sstream>
#include <string>
#include <vector>

using namespace std;
using namespace p2t;

bool ParseFile(string filename, vector<Point*>& out_polyline, vector<vector<Point*>>& out_holes,
               vector<Point*>& out_steiner);
void GenerateRandomPointDistribution(size_t num_points, double min, double max,
                                     vector<Point*>& out_polyline,
                                     vector<vector<Point*>>& out_holes,
                                     vector<Point*>& out_steiner);
void Init();
void ShutDown(int return_code);
void MainLoop(const double zoom);
void Draw(const double zoom);
void DrawMap(const double zoom);
void ConstrainedColor(bool constrain);
double StringToDouble(const std::string& s);
double Random(double (*fun)(double), double xmin, double xmax);
double Fun(double x);

double rotate_y = 0.0,
       rotate_z = 0.0;
const double rotations_per_tick = 0.2;

/// Screen center x
double cx = 0.0;
/// Screen center y
double cy = 0.0;

/// Constrained triangles
vector<Triangle*> triangles;
/// Triangle map
list<Triangle*> map;
/// Polylines
vector<Point*> polyline;
vector<vector<Point*>> holes;
vector<Point*> steiner;

/// Draw the entire triangle map?
bool draw_map = false;
/// Create a random distribution of points?
bool random_distribution = false;

GLFWwindow* window = NULL;

template <class C> void FreeClear(C& cntr)
{
  for (typename C::iterator it = cntr.begin(); it != cntr.end(); ++it) {
    delete *it;
  }
  cntr.clear();
}

int main(int argc, char* argv[])
{
  string filename;
  size_t num_points = 0u;
  double max, min;
  double zoom;

  if (argc != 5) {
    cout << "-== USAGE ==-" << endl;
    cout << "Load Data File: p2t filename center_x center_y zoom" << endl;
    cout << "Example: ./build/p2t testbed/data/dude.dat 500 500 1" << endl;
    return 1;
  }

  if (string(argv[1]) == "random") {
    num_points = atoi(argv[2]);
    random_distribution = true;
    char* pEnd;
    max = strtod(argv[3], &pEnd);
    min = -max;
    cx = cy = 0.0;
    zoom = atof(argv[4]);
  } else {
    filename = string(argv[1]);
    cx = atof(argv[2]);
    cy = atof(argv[3]);
    zoom = atof(argv[4]);
  }

  if (random_distribution) {
    GenerateRandomPointDistribution(num_points, min, max, polyline, holes, steiner);
  } else {
    // Load pointset from file
    if (!ParseFile(filename, polyline, holes, steiner)) {
      return 2;
    }
  }

  Init();

  /*
   * Perform triangulation!
   */

  double init_time = glfwGetTime();

  /*
   * STEP 1: Create CDT and add primary polyline
   * NOTE: polyline must be a simple polygon. The polyline's points
   * constitute constrained edges. No repeat points!!!
   */
  CDT* cdt = new CDT(polyline);

  /*
   * STEP 2: Add holes or Steiner points
   */
  for (const auto& hole : holes) {
    assert(!hole.empty());
    cdt->AddHole(hole);
  }
  for (const auto& s : steiner) {
    cdt->AddPoint(s);
  }

  /*
   * STEP 3: Triangulate!
   */
  cdt->Triangulate();

  double dt = glfwGetTime() - init_time;

  triangles = cdt->GetTriangles();
  map = cdt->GetMap();
  const size_t points_in_holes =
      std::accumulate(holes.cbegin(), holes.cend(), size_t(0),
                      [](size_t cumul, const vector<Point*>& hole) { return cumul + hole.size(); });

  cout << "Number of primary constrained edges = " << polyline.size() << endl;
  cout << "Number of holes = " << holes.size() << endl;
  cout << "Number of constrained edges in holes = " << points_in_holes << endl;
  cout << "Number of Steiner points = " << steiner.size() << endl;
  cout << "Total number of points = " << (polyline.size() + points_in_holes + steiner.size())
       << endl;
  cout << "Number of triangles = " << triangles.size() << endl;
  cout << "Elapsed time (ms) = " << dt * 1000.0 << endl;

  MainLoop(zoom);

  // Cleanup
  delete cdt;
  FreeClear(polyline);
  for (vector<Point*>& hole : holes) {
    FreeClear(hole);
  }
  FreeClear(steiner);

  ShutDown(0);
  return 0;
}

bool ParseFile(string filename, vector<Point*>& out_polyline, vector<vector<Point*>>& out_holes,
               vector<Point*>& out_steiner)
{
  enum ParserState {
    Polyline,
    Hole,
    Steiner,
  };
  ParserState state = Polyline;
  vector<Point*>* hole = nullptr;
  try {
    string line;
    ifstream myfile(filename);
    if (myfile.is_open()) {
      while (!myfile.eof()) {
        getline(myfile, line);
        if (line.empty()) {
          break;
        }
        istringstream iss(line);
        vector<string> tokens;
        copy(istream_iterator<string>(iss), istream_iterator<string>(), back_inserter(tokens));
        if (tokens.empty()) {
          break;
        } else if (tokens.size() == 1u) {
          const auto token = tokens[0];
          if (token == "HOLE") {
            state = Hole;
            out_holes.emplace_back();
            hole = &out_holes.back();
          } else if (token == "STEINER") {
            state = Steiner;
          } else {
            throw runtime_error("Invalid token [" + token + "]");
          }
        } else {
          double x = StringToDouble(tokens[0]);
          double y = StringToDouble(tokens[1]);
          switch (state) {
            case Polyline:
              out_polyline.push_back(new Point(x, y));
              break;
            case Hole:
              assert(hole != nullptr);
              hole->push_back(new Point(x, y));
              break;
            case Steiner:
              out_steiner.push_back(new Point(x, y));
              break;
            default:
              assert(0);
          }
        }
      }
    } else {
      throw runtime_error("File not opened");
    }
  } catch (exception& e) {
    cerr << "Error parsing file: " << e.what() << endl;
    return false;
  }
  return true;
}

void GenerateRandomPointDistribution(size_t num_points, double min, double max,
                                     vector<Point*>& out_polyline,
                                     vector<vector<Point*>>& out_holes, vector<Point*>& out_steiner)
{
  out_polyline.push_back(new Point(min, min));
  out_polyline.push_back(new Point(min, max));
  out_polyline.push_back(new Point(max, max));
  out_polyline.push_back(new Point(max, min));

  max -= (1e-4);
  min += (1e-4);
  for (int i = 0; i < num_points; i++) {
    double x = Random(Fun, min, max);
    double y = Random(Fun, min, max);
    out_steiner.push_back(new Point(x, y));
  }
}

void Init()
{
  const int window_width = 800,
            window_height = 600;

  if (glfwInit() != GL_TRUE)
    ShutDown(1);
  // 800 x 600, 16 bit color, no depth, alpha or stencil buffers, windowed
  window = glfwCreateWindow(window_width, window_height, "Poly2Tri - C++", NULL, NULL);
  if (!window)
    ShutDown(1);

  glfwMakeContextCurrent(window);
  glfwSwapInterval(1);

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
}

void ShutDown(int return_code)
{
  glfwTerminate();
  exit(return_code);
}

void MainLoop(const double zoom)
{
  // the time of the previous frame
  double old_time = glfwGetTime();
  // this just loops as long as the program runs
  bool running = true;

  while (running) {
    glfwPollEvents();

    // calculate time elapsed, and the amount by which stuff rotates
    double current_time = glfwGetTime(),
           delta_rotate = (current_time - old_time) * rotations_per_tick * 360.0;
    old_time = current_time;

    // escape to quit, arrow keys to rotate view
    // Check if ESC key was pressed or window was closed
    running = !glfwGetKey(window, GLFW_KEY_ESCAPE) && !glfwWindowShouldClose(window);

    if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS)
      rotate_y += delta_rotate;
    if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS)
      rotate_y -= delta_rotate;
    // z axis always rotates
    rotate_z += delta_rotate;

    // Draw the scene
    if (draw_map) {
      DrawMap(zoom);
    } else {
      Draw(zoom);
    }

    // swap back and front buffers
    glfwSwapBuffers(window);
  }
}

void ResetZoom(double zoom, double cx, double cy, double width, double height)
{
  double left = -width / zoom;
  double right = width / zoom;
  double bottom = -height / zoom;
  double top = height / zoom;

  // Reset viewport
  glLoadIdentity();
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  // Reset ortho view
  glOrtho(left, right, bottom, top, 1.0, -1.0);
  glTranslated(-cx, -cy, 0.0);
  glMatrixMode(GL_MODELVIEW);
  glDisable(GL_DEPTH_TEST);
  glLoadIdentity();

  // Clear the screen
  glClear(GL_COLOR_BUFFER_BIT);
}

void Draw(const double zoom)
{
  // reset zoom
  Point center = Point(cx, cy);

  ResetZoom(zoom, center.x, center.y, 800, 600);

  for (int i = 0; i < triangles.size(); i++) {
    Triangle& t = *triangles[i];
    Point& a = *t.GetPoint(0);
    Point& b = *t.GetPoint(1);
    Point& c = *t.GetPoint(2);

    // Red
    glColor3f(1, 0, 0);

    glBegin(GL_LINE_LOOP);
    glVertex2d(a.x, a.y);
    glVertex2d(b.x, b.y);
    glVertex2d(c.x, c.y);
    glEnd();
  }

  // green
  glColor3f(0, 1, 0);

  vector<vector<Point*>*> polylines;
  polylines.push_back(&polyline);
  for (vector<Point*>& hole : holes) {
      polylines.push_back(&hole);
  }
  for(int i = 0; i < polylines.size(); i++) {
    const vector<Point*>& poly = *polylines[i];
    glBegin(GL_LINE_LOOP);
      for(int j = 0; j < poly.size(); j++) {
        glVertex2d(poly[j]->x, poly[j]->y);
      }
    glEnd();
  }
}

void DrawMap(const double zoom)
{
  // reset zoom
  Point center = Point(cx, cy);

  ResetZoom(zoom, center.x, center.y, 800, 600);

  list<Triangle*>::iterator it;
  for (it = map.begin(); it != map.end(); it++) {
    Triangle& t = **it;
    Point& a = *t.GetPoint(0);
    Point& b = *t.GetPoint(1);
    Point& c = *t.GetPoint(2);

    ConstrainedColor(t.constrained_edge[2]);
    glBegin(GL_LINES);
    glVertex2d(a.x, a.y);
    glVertex2d(b.x, b.y);
    glEnd( );

    ConstrainedColor(t.constrained_edge[0]);
    glBegin(GL_LINES);
    glVertex2d(b.x, b.y);
    glVertex2d(c.x, c.y);
    glEnd( );

    ConstrainedColor(t.constrained_edge[1]);
    glBegin(GL_LINES);
    glVertex2d(c.x, c.y);
    glVertex2d(a.x, a.y);
    glEnd( );
  }
}

void ConstrainedColor(bool constrain)
{
  if (constrain) {
    // Green
    glColor3f(0, 1, 0);
  } else {
    // Red
    glColor3f(1, 0, 0);
  }
}



double StringToDouble(const std::string& s)
{
  std::istringstream i(s);
  double x;
  if (!(i >> x))
    return 0;
  return x;
}

double Fun(double x)
{
  return 2.5 + sin(10 * x) / x;
}

double Random(double (*fun)(double), double xmin = 0, double xmax = 1)
{
  static double (*Fun)(double) = NULL, YMin, YMax;
  static bool First = true;

  // Initialises random generator for first call
  if (First)
  {
    First = false;
    srand((unsigned) time(NULL));
  }

  // Evaluates maximum of function
  if (fun != Fun)
  {
    Fun = fun;
    YMin = 0, YMax = Fun(xmin);
    for (int iX = 1; iX < RAND_MAX; iX++)
    {
      double X = xmin + (xmax - xmin) * iX / RAND_MAX;
      double Y = Fun(X);
      YMax = Y > YMax ? Y : YMax;
    }
  }

  // Gets random values for X & Y
  double X = xmin + (xmax - xmin) * rand() / RAND_MAX;
  double Y = YMin + (YMax - YMin) * rand() / RAND_MAX;

  // Returns if valid and try again if not valid
  return Y < fun(X) ? X : Random(Fun, xmin, xmax);
}