//
// This file is part of MorphoGraphX - http://www.MorphoGraphX.org
// Copyright (C) 2012-2015 Richard S. Smith and collaborators.
//
// If you use MorphoGraphX in your work, please cite:
//   http://dx.doi.org/10.7554/eLife.05864
//
// MorphoGraphX is free software, and is licensed under under the terms of the 
// GNU General (GPL) Public License version 2.0, http://www.gnu.org/licenses.
// 
#include "CutSurf.hpp"

#include "ImageData.hpp"
#include "Information.hpp"
#include "Shader.hpp"
#include "Stack.hpp"
#include "Vector.hpp"

#ifndef GLAPIENTRY
#  define GLAPIENTRY
#endif

namespace mgx {

  using Information::err;
  using qglviewer::Vec;
  
  typedef Vector<16, double> Point16d;
  
  CutSurf::CutSurf() : QObject(0), cut(0), SceneRadius(-1) {}
  
  CutSurf::~CutSurf() {}
  
  // Read cutting plane parameters
  void CutSurf::readParms(Parms& parms, QString section)
  {
    bool DrawCutSurf, CutSurfGrid;
    int Mode;
    Point3f Size;
    parms(section, "DrawCutSurf", DrawCutSurf, false);
    if(DrawCutSurf)
      cut->show();
    else
      cut->hide();
    parms(section, "Mode", Mode, (int)CuttingSurface::PLANE);
    cut->setMode((CuttingSurface::Mode)Mode);
    parms(section, "CutSurfGrid", CutSurfGrid, false);
    if(CutSurfGrid)
      cut->showGrid();
    else
      cut->hideGrid();
    parms(section, "Material", Material, 0u);
    parms(section, "Size", Size, Point3f(1.0f, 1.0f, 1.0f));
    cut->setSize(Size);
    parms(section, "LineWidth", LineWidth, 1.0f);
	  Point2i SurfSize;// is this actually needed?   
    parms(section, "SurfSize", SurfSize, Point2i(15, 15));
    cut->setSurfSize(SurfSize);

    Matrix4d m = 1;
    parms(section, "Frame", m, m);
    cut->frame().setFromMatrix(m.c_data());
    cut->hasChanged();
 
 		cut->bezier().readParms(parms, section);
    selectV.clear();
		
    /*
     *  emit DrawCutSurfSignal(DrawCutSurf);
     *  emit ThreeAxisSignal(ThreeAxis);
     *  emit CutSurfGridSignal(CutSurfGrid);
     *  emit CutSurfPlaneSignal(CutSurfPlane);
     *  emit CutSurfBezierSignal(CutSurfBezier);
     *  emit SizeXSignal(int(Size.x() * 10000.0));
     *  emit SizeYSignal(int(Size.y() * 10000.0));
     *  emit SizeZSignal(int(Size.z() * 10000.0));
     */
  }
  
  // Write cutting plane parameters
  void CutSurf::writeParms(QTextStream& pout, QString section)
  {
    pout << endl;
    pout << "[" << section << "]" << endl;
    pout << "DrawCutSurf: " << (cut->isVisible() ? "true" : "false") << endl;
    pout << "Mode: " << (int)cut->mode() << endl;
    pout << "CutSurfGrid: " << (cut->drawGrid() ? "true" : "false") << endl;
    pout << "Material: " << Material << endl;
    pout << "Size: " << cut->size() << endl;
    pout << "SurfSize: " << cut->surfSize() << endl;
    pout << "LineWidth: " << LineWidth << endl;
    pout << "BezPoints: " << cut->bezier().bezPoints() << endl;
    pout << "Frame: " << Point16d(cut->frame().matrix()) << endl;
    for(size_t i = 0; i < cut->bezier().bezPoints().x() * cut->bezier().bezPoints().y(); i++)
      pout << "BezPointList: " << cut->bezier().bezierV()[i] << endl;
  }
  
  
  // Draw cutting surface slot
  void CutSurf::DrawCutSurfSlot(bool val)
  {
    if(val)
      cut->show();
    else
      cut->hide();
    emit ViewerUpdateSignal();
  }
  
  // Three axis slot
  void CutSurf::ThreeAxisSlot(bool val)
  {
    if(val)
      cut->setMode(CuttingSurface::THREE_AXIS);
    emit ViewerUpdateSignal();
  }
  
  // Draw surface grid
  void CutSurf::CutSurfGridSlot(bool val)
  {
    if(val)
      cut->showGrid();
    else
      cut->hideGrid();
    emit ViewerUpdateSignal();
  }
  
  // Draw cutting plane slot
  void CutSurf::CutSurfPlaneSlot(bool val)
  {
    if(val)
      cut->setMode(CuttingSurface::PLANE);
    emit ViewerUpdateSignal();
  }
  
  // Draw cutting Bezier slot
  void CutSurf::CutSurfBezierSlot(bool val)
  {
    if(val)
      cut->setMode(CuttingSurface::BEZIER);
    emit ViewerUpdateSignal();
  }
  
  float CutSurf::getSize(int val) {
    return 0.5f * SceneRadius * (float)exp(double(val) / 2000.0);
  }
  
  // Set sizes:
  void CutSurf::SizeXSlot(int val)
  {
    Point3f s = cut->size();
    s.x() = getSize(val);
    cut->setSize(s);
    emit ViewerUpdateSignal();
  }
  
  void CutSurf::SizeYSlot(int val)
  {
    Point3f s = cut->size();
    s.y() = getSize(val);
    cut->setSize(s);
    emit ViewerUpdateSignal();
  }
  
  void CutSurf::SizeZSlot(int val)
  {
    Point3f s = cut->size();
    s.z() = getSize(val);
    cut->setSize(s);
    emit ViewerUpdateSignal();
  }
  
  void CutSurf::drawSurface(ImgData& stk, bool select)
  {
    switch(cut->mode()) {
    case CuttingSurface::PLANE:
    case CuttingSurface::THREE_AXIS: {
      // Draw surface with texture
      int axis = (cut->mode() == CuttingSurface::THREE_AXIS ? 3 : 1);
  
      typedef void (GLAPIENTRY * colorFct_t)(const GLfloat*);
      colorFct_t colorFct = select ? glColor3fv : glTexCoord3fv;
      // void (*colorFct)(const GLfloat *) = select ? &glColor3fv : &glTexCoord3fv;
  
      const Point3f& Size = cut->size();
  
      glBegin(GL_QUADS);
      for(int i = 0; i < axis; i++) {
        Point3f x(0, 0, 0), y(0, 0, 0), z(0, 0, 0);
        x[i] = Size[i];
        y[(i + 1) % 3] = Size[(i + 1) % 3];
        z[(i + 2) % 3] = Size[(i + 2) % 3];
  
        glNormal3fv(z.c_data());
        Point3f tr = x + y;
        Point3f tl = -x + y;
        Point3f bl = -x - y;
        Point3f br = x - y;
  
        colorFct(Point3f(stk.getFrame().coordinatesOf(cut->frame().inverseCoordinatesOf(Vec(tr)))).c_data());
        glVertex3fv(tr.c_data());
        colorFct(Point3f(stk.getFrame().coordinatesOf(cut->frame().inverseCoordinatesOf(Vec(tl)))).c_data());
        glVertex3fv(tl.c_data());
        colorFct(Point3f(stk.getFrame().coordinatesOf(cut->frame().inverseCoordinatesOf(Vec(bl)))).c_data());
        glVertex3fv(bl.c_data());
        colorFct(Point3f(stk.getFrame().coordinatesOf(cut->frame().inverseCoordinatesOf(Vec(br)))).c_data());
        glVertex3fv(br.c_data());
      }
      glEnd();
    } break;
    case CuttingSurface::BEZIER: {
      const Bezier &bezier = cut->bezier();
      const Point2u& BezLines = bezier.bezLines();
      uint uSize = BezLines.x(), vSize = BezLines.y();
      glEnable(GL_AUTO_NORMAL);
      for(uint u = 0; u < uSize - 1; u++) {
        glBegin(GL_TRIANGLE_STRIP);
        for(uint v = 0; v < vSize; v++) {
          Point3f v0(bezier.evalCoord(float(u) / float(uSize - 1), float(v) / float(vSize - 1)));
          Point3f v1(bezier.evalCoord(float(u + 1) / float(uSize - 1), float(v) / float(vSize - 1)));
  
          if(select) {
            // For select we'll use the tex coords for the color
            Point3f col;
            col = Point3f((Point3f(stk.getFrame().coordinatesOf(cut->frame().inverseCoordinatesOf(Vec(v0))))
                           + Point3f(.5f, .5f, .5f)).c_data());
            glColor3fv(col.c_data());
            glVertex3fv(v0.c_data());
  
            col = Point3f((Point3f(stk.getFrame().coordinatesOf(cut->frame().inverseCoordinatesOf(Vec(v1))))
                           + Point3f(.5f, .5f, .5f)).c_data());
            glColor3fv(col.c_data());
            glVertex3fv(v1.c_data());
          } else {
            glTexCoord3fv(
              Point3f(stk.getFrame().coordinatesOf(cut->frame().inverseCoordinatesOf(Vec(v0)))).c_data());
            glVertex3fv(v0.c_data());
            glTexCoord3fv(
              Point3f(stk.getFrame().coordinatesOf(cut->frame().inverseCoordinatesOf(Vec(v1)))).c_data());
            glVertex3fv(v1.c_data());
          }
        }
        glEnd();
      }
    }
    }
  }
  
  // Draw cutting plane
  void CutSurf::drawCutSurf(ImgData& stk, bool select, Shader* shader)
  {
    if(!cut->isVisible() or !stk.isVisible())
      return;
  
    glDisable(GL_BLEND);
    glDisable(GL_TEXTURE_1D);
    glDisable(GL_TEXTURE_2D);
    glDisable(GL_TEXTURE_3D);
    glDisable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_LIGHTING);
  
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glMultMatrixd(cut->frame().matrix());
  
    if(select) {
      glClearColor(0, 0, 0, 1.0);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      drawSurface(stk, true);
    } else if(stk.isVisible()) {
  
      glEnable(GL_LIGHTING);
      glFrontFace(GL_CCW);
      glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
      stk.setup3DRenderingData(shader);
  
      shader->setUniform("brightness", GLSLValue(powf(stk.mesh->brightness(), 2.0f)));
      shader->setUniform("opacity", GLSLValue(stk.mesh->opacity()));
      shader->setUniform("second_brightness", GLSLValue(powf(stk.mesh->brightness(), 2.0f)));
      shader->setUniform("second_opacity", GLSLValue(stk.mesh->opacity()));
      shader->setUniform("blending", GLSLValue(stk.mesh->blending()));
  
      // First, draw main
      shader->useShaders();
      shader->setupUniforms();
      drawSurface(stk, false);
      shader->stopUsingShaders();
  
      stk.unbind3DTex();
		}

		glPopMatrix();
  }
  
  void CutSurf::drawCutSurfGrid(ImgData& stk)
  {
    if(!cut->drawGrid())
      return;
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glMultMatrixd(cut->frame().matrix());
    glDisable(GL_LIGHTING);
    glDisable(GL_TEXTURE_3D);
    glDisable(GL_CULL_FACE);
  
    glColor4fv(Colors::getColor(Colors::CuttingPlaneGridColor).c_data());
  
    // Draw the grid
    switch(cut->mode()) {
    case CuttingSurface::PLANE:
    case CuttingSurface::THREE_AXIS: {
      const Point3f& Size = cut->size();
      int axis = (cut->mode() == CuttingSurface::THREE_AXIS ? 3 : 1);
      glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
      glLineWidth(LineWidth);
      for(int i = 0; i < axis; i++) {
        Point3f x(0, 0, 0), y(0, 0, 0), z(0, 0, 0);
        x[i] = Size[i];
        y[(i + 1) % 3] = Size[(i + 1) % 3];
        z[(i + 2) % 3] = Size[(i + 2) % 3];
  
        glNormal3fv(z.c_data());
        Point3f tr = x + y;
        Point3f tl = -x + y;
        Point3f bl = -x - y;
        Point3f br = x - y;
  
        glBegin(GL_QUADS);
        glVertex3fv(tr.c_data());
        glVertex3fv(tl.c_data());
        glVertex3fv(bl.c_data());
        glVertex3fv(br.c_data());
        glEnd();
      }
    } break;
    case CuttingSurface::BEZIER: {
      const Bezier &bezier = cut->bezier();
      glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
      glLineWidth(LineWidth);
      //int uSize = cut->surfSize().x(), vSize = cut->surfSize().y();
      int uSize = bezier.bezLines().x(), vSize = bezier.bezLines().y();
      glBegin(GL_QUADS);
      for(int u = 0; u < uSize - 1; u++) {
        for(int v = 0; v < vSize; v++) {
          Point3f tl(bezier.evalCoord(float(u) / float(uSize - 1), float(v) / float(vSize - 1)));
          Point3f bl(bezier.evalCoord(float(u) / float(uSize - 1), float(v + 1) / float(vSize - 1)));
          Point3f br(bezier.evalCoord(float(u + 1) / float(uSize - 1), float(v + 1) / float(vSize - 1)));
          Point3f tr(bezier.evalCoord(float(u + 1) / float(uSize - 1), float(v) / float(vSize - 1)));
  
          glVertex3fv(tl.c_data());
          glVertex3fv(bl.c_data());
          glVertex3fv(br.c_data());
          glVertex3fv(tr.c_data());
        }
      }
      glEnd();
    }
    }
  
    // Draw the points if required
    if(cut->mode() == CuttingSurface::BEZIER and stk.mesh->showMeshPoints() and cut->bezier().bezPoints().x()) {
      glPointSize(ImgData::MeshPointSize);
      Color3f meshcol = Colors::getColor(stk.MeshColor);
      Color3f selectcol = Colors::getColor(stk.MeshSelectColor);
  
      const Bezier &bezier = cut->bezier();
      glBegin(GL_POINTS);
      for(uint v = 0; v < bezier.bezPoints().y(); v++)
        for(uint u = 0; u < bezier.bezPoints().x(); u++) {
          if(selectV.find(bezier.idx(u, v)) == selectV.end())
            glColor3fv(meshcol.data());
          else
            glColor3fv(selectcol.data());
          glVertex3dv(bezier.bezierV(u, v).c_data());
        }
      glEnd();
    }
    glPopMatrix();
  }
  
  // Clear list of selected points
  void CutSurf::clearSelect() 
  {
    selectV.clear();
  }
  
  bool CutSurf::showOpaqueSurface(const ImgData& stk)
  {
    return stk.isVisible() and cut->isVisible() and not stk.mesh->blending();
  }
  
  bool CutSurf::showTransparentSurface(const ImgData& stk)
  {
    return stk.isVisible() and cut->isVisible() and stk.mesh->blending();
  }
  
  void CutSurf::Reset(double sceneRadius)
  {
    SceneRadius = sceneRadius;
    cut->setSize(Point3f(SceneRadius, SceneRadius, SceneRadius));
  
    if(SceneRadius > 0.0f and (cut->isVisible() or cut->drawGrid())) {
      switch(cut->mode()) {
      case CuttingSurface::BEZIER: {
				cut->bezier().resetBezier();
				cut->bezier().setBezSize(Point2d(SceneRadius,SceneRadius));
        cut->bezier().initBez();
      }
      default:
        break;
      }
    }
    emit ViewerUpdateSignal();
  }
  
  void CutSurf::setSceneBoundingBox(const Point3f& bbox)
  {
    if(SceneRadius <= 0)
      SceneRadius = norm(bbox);
  }

}
