#include "config.h"
#include "Scene_c3t3_item.h"
#include "Scene_surface_mesh_item.h"

#include <QVector>
#include <QColor>
#include <QPixmap>
#include <QApplication>
#include <QPainter>
#include <QtCore/qglobal.h>
#include <QGuiApplication>
#include <QSlider>
#include <QWidgetAction>
#include <QKeyEvent>
#include <QMouseEvent>

#include <map>
#include <vector>

#include <CGAL/Three/Scene_interface.h>
#include <CGAL/Three/Triangle_container.h>
#include <CGAL/Three/Edge_container.h>
#include <CGAL/Three/Point_container.h>
#include <CGAL/Three/Three.h>

#include <CGAL/Real_timer.h>

#include <CGAL/Qt/manipulatedFrame.h>
#include <CGAL/Qt/qglviewer.h>

#include <boost/iterator/function_output_iterator.hpp>
#include <boost/range/empty.hpp>

#include <CGAL/IO/io.h>
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangulation_3_cell_primitive.h>
#include <CGAL/facets_in_complex_3_to_triangle_mesh.h>

#include "Scene_polygon_soup_item.h"


typedef CGAL::AABB_triangulation_3_cell_primitive<EPICK,
                                                  C3t3::Triangulation> Primitive;
typedef CGAL::AABB_traits_3<EPICK, Primitive> Traits;
typedef CGAL::AABB_tree<Traits> Tree;
typedef Tree::Point_and_primitive_id Point_and_primitive_id;
using namespace CGAL::Three;
typedef Triangle_container Tc;
typedef Edge_container Ec;
typedef Point_container Pc;
typedef Viewer_interface Vi;


struct Scene_c3t3_item_priv {

  void init_default_values() {
    sharp_edges_angle = -1;
    detect_borders = false;
  }

  Scene_c3t3_item_priv(Scene_c3t3_item* item)
    : item(item), c3t3(), is_valid(true), computed_stats(false){
  }

  Scene_c3t3_item_priv(const C3t3& c3t3_, Scene_c3t3_item* item)
    : item(item), c3t3(c3t3_), is_valid(true), computed_stats(false){
  }

  ~Scene_c3t3_item_priv()
  {
    c3t3.clear();
  }

  void draw_triangle_edges_cnc(const Tr::Bare_point& pa,
                               const Tr::Bare_point& pb,
                               const Tr::Bare_point& pc) const;
  Scene_c3t3_item* item;
  C3t3 c3t3;
  bool is_surface;
  //only for optimizers
  double sharp_edges_angle;
  bool detect_borders;
  bool is_valid;
  bool cnc_are_shown;

  enum STATS {
    MIN_EDGES_LENGTH = 0,
    MAX_EDGES_LENGTH,
    MEAN_EDGES_LENGTH,
    MIN_DIHEDRAL_ANGLE,
    MAX_DIHEDRAL_ANGLE,
    MEAN_DIHEDRAL_ANGLE,
    NB_SPHERES,
    NB_VERTICES,
    NB_TETS,
    SMALLEST_RAD_RAD,
    SMALLEST_EDGE_RAD,
    BIGGEST_VL3_CUBE,
    NB_SUBDOMAINS,
    NB_CNC
  };

  mutable std::vector<float> positions_lines_not_in_complex;
  mutable std::size_t positions_lines_not_in_complex_size;
  mutable std::size_t nb_cnc;
  mutable bool computed_stats;

  void push_point(std::vector<float>& points, const EPICK::Point_3& p,
                  const CGAL::qglviewer::Vec& offset) const
  {
    points.push_back(static_cast<float>(p.x()+offset.x));
    points.push_back(static_cast<float>(p.y()+offset.y));
    points.push_back(static_cast<float>(p.z()+offset.z));
  }

  void push_edge(std::vector<float>& edges,
                 const EPICK::Point_3& pa,
                 const EPICK::Point_3& pb,
                 const CGAL::qglviewer::Vec& offset) const
  {
    push_point(edges, pa, offset);
    push_point(edges, pb, offset);
  }
};

void Scene_c3t3_item_priv::draw_triangle_edges_cnc(const Tr::Bare_point& pa,
                                                   const Tr::Bare_point& pb,
                                                   const Tr::Bare_point& pc) const
{
  const CGAL::qglviewer::Vec offset = Three::mainViewer()->offset();
  push_edge(positions_lines_not_in_complex, pa, pb, offset);
  push_edge(positions_lines_not_in_complex, pb, pc, offset);
  push_edge(positions_lines_not_in_complex, pc, pa, offset);
}


void Scene_c3t3_item::common_constructor(bool is_surface)
{
  d->is_surface = is_surface;
  d->cnc_are_shown = false;
  setEdgeContainer(CNC, new Ec(Vi::PROGRAM_NO_SELECTION, false));
}

Scene_c3t3_item::Scene_c3t3_item(bool is_surface)
  : Scene_triangulation_3_item(!is_surface)
{
  d = new Scene_c3t3_item_priv(this);
  common_constructor(is_surface);

}

Scene_c3t3_item::Scene_c3t3_item(const C3t3& c3t3, bool is_surface)
  : Scene_triangulation_3_item(c3t3.triangulation(), !is_surface)
{
  d = new Scene_c3t3_item_priv(c3t3, this);
  common_constructor(is_surface);
}

Scene_c3t3_item::~Scene_c3t3_item()
{
  delete d;
}

const C3t3&
Scene_c3t3_item::c3t3() const {
  return d->c3t3;
}

C3t3&
Scene_c3t3_item::c3t3()
{
  return d->c3t3;
}

void
Scene_c3t3_item::c3t3_changed()
{
  d->c3t3.rescan_after_load_of_triangulation();
  triangulation_changed();
}

const T3&
Scene_c3t3_item::triangulation() const {
  return d->c3t3.triangulation();
}

T3&
Scene_c3t3_item::triangulation()
{
  return d->c3t3.triangulation();
}

bool Scene_c3t3_item::do_take_cell(const T3::Cell_handle& c) const
{
  return d->c3t3.is_in_complex(c);
}

bool Scene_c3t3_item::do_take_facet(const T3::Facet& f)const
{
  return (d->c3t3.is_in_complex(f));
}

bool Scene_c3t3_item::do_take_vertex(const T3::Vertex_handle& v)const
{
  return d->c3t3.is_in_complex(v);
}

bool Scene_c3t3_item::is_facet_oriented(const T3::Facet& f)const
{
  const Tr::Cell_handle& cell = f.first;
  const int& index = f.second;
  return (index % 2 == 1) == d->c3t3.is_in_complex(cell);
}

QString Scene_c3t3_item::toolTip() const {
  return tr("<p><b>3D complex in a 3D triangulation</b></p>"
    "<p>Number of vertices: %1<br />"
    "Number of surface facets: %2<br />"
    "Number of volume tetrahedra: %3</p>%4")
    .arg(c3t3().triangulation().number_of_vertices())
    .arg(c3t3().number_of_facets_in_complex())
    .arg(c3t3().number_of_cells_in_complex())
    .arg(property("toolTip").toString());
}
QMenu* Scene_c3t3_item::contextMenu()
{
  const char* prop_name = "Menu modified by Scene_c3t3_item.";

  QMenu* menu = Scene_triangulation_3_item::contextMenu();

  // Use dynamic properties:
  // https://doc.qt.io/qt-5/qobject.html#property
  bool menuChanged = menu->property(prop_name).toBool();

  if (!menuChanged) {

    if(is_valid())
    {
      QAction* actionShowCNC =
          menu->addAction(tr("Show cells not in complex"));
      actionShowCNC->setCheckable(true);
      actionShowCNC->setObjectName("actionShowCNC");
      connect(actionShowCNC, SIGNAL(toggled(bool)),
              this, SLOT(show_cnc(bool)));
    }

    QAction* actionExportFacetsInComplex =
      menu->addAction(tr("Export facets in complex"));
    actionExportFacetsInComplex->setObjectName("actionExportFacetsInComplex");
    connect(actionExportFacetsInComplex,
      SIGNAL(triggered()), this,
      SLOT(export_facets_in_complex()));

    QAction* actionResetSurfacePatches = menu->addAction(tr("Reset surface patches"));
    actionResetSurfacePatches->setObjectName("actionResetSurfacePatches");
    connect(actionResetSurfacePatches,
            SIGNAL(triggered()),
            this,
            SLOT(reset_surface_patches()));

    menu->setProperty(prop_name, true);
  }
  return menu;
}

bool Scene_c3t3_item::is_valid() const
{
  return d->is_valid;
}

void Scene_c3t3_item::set_valid(bool b)
{
  d->is_valid = b;
}

void Scene_c3t3_item::show_cnc(bool b)
{
  if(is_valid())
  {
    d->cnc_are_shown = b;
    contextMenu()->findChild<QAction*>("actionShowCNC")->setChecked(b);
    Q_EMIT redraw();
  }
}

bool Scene_c3t3_item::load_binary(std::istream& is)
{
  if(!CGAL::IO::load_binary_file(is, c3t3())) return false;
  resetCutPlane();
  if(is.good()) {
    c3t3_changed();
    changed();
    return true;
  }
  else
    return false;
}

void Scene_c3t3_item::compute_bbox() const
{
  if (isEmpty())
    _bbox = Bbox();
  else
  {
    CGAL::Bbox_3 result;//default is [+infinity; -infinity]
    for (Tr::Cell_handle c : c3t3().cells_in_complex())
    {
      Tr::Vertex_handle v = (c->vertex(0) != c3t3().triangulation().infinite_vertex())
                           ? c->vertex(0)
                           : c->vertex(1);
      result += v->point().bbox();
    }
    _bbox = Bbox(result.xmin(), result.ymin(), result.zmin(),
                 result.xmax(), result.ymax(), result.zmax());

    if (boost::empty(c3t3().cells_in_complex()))
    {
      for (Tr::Vertex_handle v : c3t3().triangulation().finite_vertex_handles())
      {
        if(v->in_dimension() != -1) //skip far points
          result += v->point().bbox();
      }
      _bbox = Bbox(result.xmin(), result.ymin(), result.zmin(),
                   result.xmax(), result.ymax(), result.zmax());
    }
  }
}


void Scene_c3t3_item::export_facets_in_complex()
{
  SMesh outmesh;
  CGAL::facets_in_complex_3_to_triangle_mesh(c3t3(), outmesh);
  Scene_surface_mesh_item* item = new Scene_surface_mesh_item(std::move(outmesh));
  item->setName(QString("%1_%2").arg(this->name()).arg("facets"));
  scene->addItem(item);
  this->setVisible(false);
}

void Scene_c3t3_item::reset_surface_patches()
{
  if(c3t3().number_of_facets() == 0)
  {
    CGAL::Three::Three::warning(tr("There are no facets in the complex."));
    return;
  }

  const C3t3::Facet first_facet = *c3t3().facets_in_complex_begin();
  const C3t3::Surface_patch_index first_index = c3t3().surface_patch_index(first_facet);

  for(auto f : c3t3().facets_in_complex())
  {
    const auto mf = c3t3().triangulation().mirror_facet(f);
    f.first->set_surface_patch_index(f.second, first_index);
    mf.first->set_surface_patch_index(mf.second, first_index);
  }
  triangulation_changed();
  this->invalidateOpenGLBuffers();
  Q_EMIT itemChanged();
}

void Scene_c3t3_item::drawEdges(Viewer_interface *viewer) const
{
  Scene_triangulation_3_item::drawEdges(viewer);
//add cnc
  if(!visible())
    return;
  if(d->cnc_are_shown)
  {
    getEdgeContainer(CNC)->setColor(QColor(Qt::black));
    getEdgeContainer(CNC)->draw(viewer, true);
  }
}

void Scene_c3t3_item::initializeBuffers(CGAL::Three::Viewer_interface *viewer)const
{
  Scene_triangulation_3_item::initializeBuffers(viewer);
  // add cnc
  {
    getEdgeContainer(Scene_c3t3_item::CNC)->initializeBuffers(viewer);
    getEdgeContainer(Scene_c3t3_item::CNC)->setFlatDataSize(
          d->positions_lines_not_in_complex_size);
    d->positions_lines_not_in_complex.clear();
    d->positions_lines_not_in_complex.shrink_to_fit();
  }
}
void Scene_c3t3_item::computeElements()const
{
  Scene_triangulation_3_item::computeElements();
  QApplication::setOverrideCursor(Qt::WaitCursor);
  //add cnc
  //the cells not in the complex
  Geom_traits::Construct_point_3 wp2p
    = c3t3().triangulation().geom_traits().construct_point_3_object();
  for(C3t3::Triangulation::Cell_iterator
      cit = c3t3().triangulation().finite_cells_begin(),
      end = c3t3().triangulation().finite_cells_end();
      cit != end; ++cit)
  {
    if(!c3t3().is_in_complex(cit))
    {

      bool has_far_point = false;
      for(int i=0; i<4; i++)
        if(c3t3().in_dimension(cit->vertex(i)) == -1)
        {
          has_far_point = true;
          break;
        }
      if(!has_far_point)
      {
        const Tr::Bare_point& p1 = wp2p(cit->vertex(0)->point());
        const Tr::Bare_point& p2 = wp2p(cit->vertex(1)->point());
        const Tr::Bare_point& p3 = wp2p(cit->vertex(2)->point());
        const Tr::Bare_point& p4 = wp2p(cit->vertex(3)->point());
        d->draw_triangle_edges_cnc(p1, p2, p4);
        d->draw_triangle_edges_cnc(p1, p3, p4);
        d->draw_triangle_edges_cnc(p2, p3, p4);
        d->draw_triangle_edges_cnc(p1, p2, p3);
      }
    }
  }
  getEdgeContainer(CNC)->allocate(
        Ec::Vertices,
        d->positions_lines_not_in_complex.data(),
        static_cast<int>(d->positions_lines_not_in_complex.size()*sizeof(float)));
  d->positions_lines_not_in_complex_size = d->positions_lines_not_in_complex.size();
  QApplication::restoreOverrideCursor();
}

QString Scene_c3t3_item::computeStats(int type)
{
  if(type != Scene_c3t3_item_priv::NB_CNC)
    return Scene_triangulation_3_item::computeStats(type);
  if(!d->computed_stats)
  {
    d->nb_cnc = 0;
    for(C3t3::Triangulation::Cell_iterator
        cit = d->c3t3.triangulation().finite_cells_begin(),
        end = d->c3t3.triangulation().finite_cells_end();
        cit != end; ++cit)
    {
      if(!d->c3t3.is_in_complex(cit))
      {

        bool has_far_point = false;
        for(int i=0; i<4; i++)
          if(d->c3t3.in_dimension(cit->vertex(i)) == -1)
          {
            has_far_point = true;
            break;
          }
        if(!has_far_point)
          ++d->nb_cnc;
      }
    }
    d->computed_stats = true;
  }
  return QString::number(d->nb_cnc);
}

CGAL::Three::Scene_item::Header_data Scene_c3t3_item::header() const
{
  CGAL::Three::Scene_item::Header_data data = Scene_triangulation_3_item::header();
  data.categories[0] = std::pair<QString,int>(QString("Properties"),14);
  data.titles.append(QString("#Cells not in Complex"));
  return data;
}

bool Scene_c3t3_item::has_cnc()const { return d->cnc_are_shown;}

bool Scene_c3t3_item::is_surface() const
{
  return d->is_surface;
}

void Scene_c3t3_item::set_sharp_edges_angle(double a) { d->sharp_edges_angle = a; }
double Scene_c3t3_item::get_sharp_edges_angle() { return d->sharp_edges_angle; }

void Scene_c3t3_item::set_detect_borders(bool b) { d->detect_borders = b;}
bool Scene_c3t3_item::get_detect_borders() { return d->detect_borders; }

Scene_c3t3_item* Scene_c3t3_item::clone() const
{
  return new Scene_c3t3_item(d->c3t3, d->is_surface);
}

void Scene_c3t3_item::copyProperties(Scene_item *item)
{
  Scene_triangulation_3_item::copyProperties(item);
  Scene_c3t3_item* c3t3_item = qobject_cast<Scene_c3t3_item*>(item);
  if(!c3t3_item)
    return;
  show_cnc(c3t3_item->has_cnc());

}
