#include "config.h"

#include <CGAL/Three/CGAL_Lab_plugin_interface.h>
#include "ui_Offset_meshing_dialog.h"

#include "C3t3_type.h"

#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_face_graph_triangle_primitive.h>

#include <CGAL/Side_of_triangle_mesh.h>
#include <CGAL/Polygon_mesh_processing/bbox.h>
#include <CGAL/Polygon_mesh_processing/orientation.h>
#include <CGAL/Polygon_mesh_processing/compute_normal.h>

#include <CGAL/Timer.h>
#include <CGAL/make_mesh_3.h>
#include <CGAL/Labeled_mesh_domain_3.h>
#include <CGAL/Mesh_domain_with_polyline_features_3.h>
#include <CGAL/Mesh_criteria_3.h>
#include <CGAL/facets_in_complex_3_to_triangle_mesh.h>

#include <CGAL/Three/Three.h>

#include <QObject>
#include <QAction>
#include <QMainWindow>
#include <QMenu>
#include <QApplication>
#include <QtPlugin>
#include <QThread>
#include "Scene_surface_mesh_item.h"
#include "Scene_polygon_soup_item.h"
#include "Scene_polylines_item.h"
#include <QInputDialog>
#include <QStringList>
#include <QMessageBox>
#include <QAbstractButton>

#include <algorithm>
#include <iostream>
#include <iterator>
#include <memory>

using namespace CGAL::Three;

namespace CGAL {

template <class TriangleMesh, class GeomTraits>
class Offset_function
{
  using Primitive = AABB_face_graph_triangle_primitive<TriangleMesh>;
  using Traits = AABB_traits_3<GeomTraits, Primitive>;
  using Tree = AABB_tree<Traits>;
  using Side_of = Side_of_triangle_mesh<TriangleMesh, GeomTraits>;

  using FT = typename GeomTraits::FT;
  using Point_3 = typename GeomTraits::Point_3;

public:
  Offset_function(const TriangleMesh& tm,
                  double offset_distance)
    : m_tree_ptr(std::make_shared<Tree>(std::begin(faces(tm)), std::end(faces(tm)), tm)),
      m_side_of_ptr(std::make_shared<Side_of>(*m_tree_ptr)),
      m_is_inset(offset_distance < 0),
      m_sq_offset_distance(CGAL::square(offset_distance)),
      m_is_closed(is_closed(tm))
  {
    CGAL_assertion(!m_tree_ptr->empty());
  }

  // we only need negative inside, and positive outside, so we can compare square roots
  double operator()(const Point_3& p) const
  {
    const Bounded_side side = m_is_closed ? m_side_of_ptr->operator()(p) : ON_UNBOUNDED_SIDE;

    if(m_is_inset) // also means that the mesh is closed
    {
      // - ON_UNBOUNDED_SIDE is outside the offset since we are insetting
      // - ON_BOUNDARY is outside the offset since we are insetting
      if(side != ON_BOUNDED_SIDE)
        return 1;

      // inside the offset if the distance to the input mesh is greater than the offset distance
      const FT sq_distance = m_tree_ptr->squared_distance(p);
      return (sq_distance > m_sq_offset_distance) ? -1 : 1;
    }
    else // outset
    {
      // - ON_BOUNDED_SIDE can only happen if it's a closed mesh, and in that case, being inside
      // the mesh is being inside the offset
      // - ON_BOUNDARY is in the offset whether the mesh is open or closed
      if(side != ON_UNBOUNDED_SIDE)
        return - 1;

      // inside the offset if the distance to the input mesh is smaller than the offset distance
      const FT sq_distance = m_tree_ptr->squared_distance(p);
      return (sq_distance < m_sq_offset_distance) ? -1 : 1;
    }
  }

private:
  std::shared_ptr<Tree> m_tree_ptr;
  std::shared_ptr<Side_of> m_side_of_ptr;
  const bool m_is_inset;
  const double m_sq_offset_distance;
  const bool m_is_closed;
};

template <typename Points, typename Polygons>
class Polygon_soup_offset_function
{
  using Polygon_iterator = typename Polygons::const_iterator;

  class Polygon_soup_point_property_map
  {
    const Points* points_vector_ptr;

  public:
    using key_type = Polygon_iterator;
    using value_type = EPICK::Point_3;
    using reference = const value_type&;
    using category = boost::readable_property_map_tag;

    Polygon_soup_point_property_map() = default;
    Polygon_soup_point_property_map(const Points* ptr) : points_vector_ptr(ptr) { }

    friend reference get(Polygon_soup_point_property_map map,
                         key_type polygon_it)
    {
      return (*map.points_vector_ptr)[*polygon_it->begin()];
    }
  };

  class Polygon_soup_triangle_property_map
  {
    const Points* points_vector_ptr;

  public:
    using key_type = Polygon_iterator;
    using value_type = EPICK::Triangle_3;
    using reference = value_type;
    using category = boost::readable_property_map_tag;

    Polygon_soup_triangle_property_map() = default;
    Polygon_soup_triangle_property_map(const Points* ptr) : points_vector_ptr(ptr) { }

    friend value_type get(Polygon_soup_triangle_property_map map,
                          key_type polygon_it)
    {
      auto it = polygon_it->begin();
      CGAL_assertion(it != polygon_it->end());
      const auto id0 = *it++;
      CGAL_assertion(it != polygon_it->end());
      const auto id1 = *it++;
      CGAL_assertion(it != polygon_it->end());
      const auto id2 = *it++;
      CGAL_assertion(it == polygon_it->end());

      return value_type((*map.points_vector_ptr)[id0],
                        (*map.points_vector_ptr)[id1],
                        (*map.points_vector_ptr)[id2]);
    }
  };

  struct AABB_polygon_soup_triangle_primitive
    : public CGAL::AABB_primitive<Polygon_iterator,
                                  Polygon_soup_triangle_property_map,
                                  Polygon_soup_point_property_map,
                                  CGAL::Tag_true /*ExternalPropertyMaps*/,
                                  CGAL::Tag_false /*CacheDatum*/>
  {
    using Base = CGAL::AABB_primitive<Polygon_iterator,
                                      Polygon_soup_triangle_property_map,
                                      Polygon_soup_point_property_map,
                                      CGAL::Tag_true,
                                      CGAL::Tag_false>;

    using Id = Polygon_iterator;

    template <typename ObjectPmap, typename PointPmap>
    AABB_polygon_soup_triangle_primitive(Id id,
                                         ObjectPmap&& opmap,
                                         PointPmap&& ppmap)
      : Base(id, std::forward<ObjectPmap>(opmap), std::forward<PointPmap>(ppmap))
    {
    }

    template <typename Iterator, typename ObjectPmap, typename PointPmap>
    AABB_polygon_soup_triangle_primitive(Iterator it,
                                         ObjectPmap&& opmap,
                                         PointPmap&& ppmap)
      : Base(*it, std::forward<ObjectPmap>(opmap), std::forward<PointPmap>(ppmap))
    {
    }
  }; // struct template Polygon_soup_primitive

  using AABB_traits = CGAL::AABB_traits_3<EPICK, AABB_polygon_soup_triangle_primitive>;
  using AABB_tree = CGAL::AABB_tree<AABB_traits>;

  std::shared_ptr<AABB_tree> m_tree_ptr;
  double m_sq_offset_distance;

public:
  Polygon_soup_offset_function(const Points& points,
                               const Polygons& polygons,
                               const double offset_distance)
    : m_tree_ptr(std::make_shared<AABB_tree>(std::begin(polygons),
                                             std::end(polygons),
                                             Polygon_soup_triangle_property_map(&points),
                                             Polygon_soup_point_property_map(&points))),
      m_sq_offset_distance(square(offset_distance))
  {
    CGAL_assertion(!m_tree_ptr->empty());
  }

  // we only need negative inside, and positive outside, so we can compare square roots
  double operator()(const EPICK::Point_3& p) const
  {
    // it's a soup so it's open by definition ==> treat inset and outset identically
    const double sq_distance = m_tree_ptr->squared_distance(p);
    return sq_distance - m_sq_offset_distance;
  }

}; // class Polygon_soup_offset_function

} // namespace CGAL

CGAL::Offset_function<SMesh, EPICK>
offset_function(Scene_surface_mesh_item* item, double offset_value)
{
  return { *(item->face_graph()), offset_value };
}

CGAL::Polygon_soup_offset_function<Scene_polygon_soup_item::Points,
                                   Scene_polygon_soup_item::Polygons>
offset_function(Scene_polygon_soup_item* item, double offset_value)
{
  return { item->points(), item->polygons(), offset_value };
}

class MeshGuard
{
  SMesh* mesh;
  bool done;

public:
  MeshGuard(SMesh* mesh) : mesh(mesh), done(false) { }
  void setDone() { done = true; }
  ~MeshGuard()
  {
    if(!done)
      delete mesh;
  }
};

// declare the CGAL function
template<class SourceItem>
SMesh* cgal_off_meshing(QWidget*,
                        SourceItem* source_item,
                        Scene_polylines_item* polylines_item,
                        const double offset_value,
                        const double angle,
                        const double sizing,
                        const double approx,
                        const double edge_size,
                        int tag)
{
  using GT = EPICK;
  using Sphere_3 = GT::Sphere_3;

  using Mesh_domain_base = CGAL::Labeled_mesh_domain_3<GT, int, int>;
  using Mesh_domain = CGAL::Mesh_domain_with_polyline_features_3<Mesh_domain_base>;
  using Tr = C3t3::Triangulation;
  using Mesh_criteria = CGAL::Mesh_criteria_3<Tr>;

  const CGAL::Bbox_3 bbox = source_item->bbox();

  const GT::Point_3 center((bbox.xmax() + bbox.xmin()) / 2,
                           (bbox.ymax() + bbox.ymin()) / 2,
                           (bbox.zmax() + bbox.zmin()) / 2);
  const double rad = 0.6 * std::sqrt(CGAL::square(bbox.xmax() - bbox.xmin()) +
                                     CGAL::square(bbox.ymax() - bbox.ymin()) +
                                     CGAL::square(bbox.zmax() - bbox.zmin()))
                     + offset_value;
  const double sqrad = CGAL::square(rad);

  CGAL::Timer timer;
  timer.start();

  namespace p = CGAL::parameters;

  Mesh_domain domain =
    Mesh_domain::create_implicit_mesh_domain
    (p::function = offset_function(source_item, offset_value),
     p::bounding_object = Sphere_3(center, sqrad),
     p::relative_error_bound = 1e-7,
     p::construct_surface_patch_index = [](int i, int j) { return (i * 1000 + j); });

  const CGAL::Mesh_facet_topology topology = CGAL::FACET_VERTICES_ON_SAME_SURFACE_PATCH;
  auto manifold_option = p::non_manifold();
  if(tag == 1)
    manifold_option = p::manifold_with_boundary();
  if(tag == 2)
    manifold_option = p::manifold();

  Mesh_criteria criteria(p::facet_angle = angle,
                         p::facet_size = sizing,
                         p::facet_distance = approx,
                         p::facet_topology = topology,
                         p::edge_size = edge_size);

  if(polylines_item != nullptr)
  {
    typedef std::vector<Mesh_domain::Surface_patch_index> Surface_patch_ids;
    std::vector<Mesh_domain::Surface_patch_index> surface_patch_ids;

    domain.add_features_and_incidences(polylines_item->polylines.begin(),
                                       polylines_item->polylines.end(),
                                       CGAL::Identity_property_map<Scene_polylines_item::Polyline>(),
                                       CGAL::Constant_property_map<Scene_polylines_item::Polyline, Surface_patch_ids>(surface_patch_ids));
  }

  C3t3 c3t3 = CGAL::make_mesh_3<C3t3>(domain, criteria,
                                      p::no_perturb(),
                                      p::no_exude(),
                                      manifold_option);

  timer.stop();
  std::cerr << "done (" << timer.time() << " ms, " << c3t3.triangulation().number_of_vertices() << " vertices)" << std::endl;

  if(c3t3.number_of_facets_in_complex() > 0)
  {
    SMesh* pRemesh = new SMesh();

    // if the thread is interrupted before the mesh is returned, delete it.
    MeshGuard guard(pRemesh);
    CGAL::facets_in_complex_3_to_triangle_mesh(c3t3, *pRemesh);
    guard.setDone();

    CGAL_postcondition(CGAL::Polygon_mesh_processing::is_outward_oriented(*pRemesh));

    return pRemesh;
  }
  else
  {
    return nullptr;
  }
}

struct Mesher_thread
  : public QThread
{
  Q_OBJECT

private:
  Scene_surface_mesh_item* sm_item;
  Scene_polygon_soup_item* soup_item;
  Scene_polylines_item* polylines_item;

  const double offset_value;
  const double angle;
  const double sizing;
  const double approx;
  const double edge_size;
  int tag_index;

public:
  Mesher_thread(Scene_surface_mesh_item* sm_item,
                Scene_polygon_soup_item* soup_item,
                Scene_polylines_item* polylines_item,
                const double offset_value,
                const double angle,
                const double sizing,
                const double approx,
                const double edge_size,
                int tag)
    : sm_item(sm_item), soup_item(soup_item), polylines_item(polylines_item),
      offset_value(offset_value),
      angle(angle), sizing(sizing), approx(approx), edge_size(edge_size), tag_index(tag)
  {
  }

  void run() override
  {
    SMesh* offset_mesh = nullptr;

    if(soup_item)
    {
      offset_mesh = cgal_off_meshing(Three::mainWindow(),
                                     soup_item, polylines_item,
                                     offset_value,
                                     angle, sizing, approx, edge_size, tag_index);
    }
    else
    {
      offset_mesh = cgal_off_meshing(Three::mainWindow(),
                                     sm_item, polylines_item,
                                     offset_value,
                                     angle, sizing, approx, edge_size, tag_index);
    }

    Three::getMutex()->lock();
    Three::getWaitCondition()->wakeAll();
    Three::getMutex()->unlock();

    Q_EMIT resultReady(offset_mesh);
  }

Q_SIGNALS:
  void resultReady(SMesh *offset_mesh);
};

class CGAL_Lab_offset_meshing_plugin
  : public QObject,
    protected CGAL_Lab_plugin_interface
{
  Q_OBJECT
  Q_INTERFACES(CGAL::Three::CGAL_Lab_plugin_interface)
  Q_PLUGIN_METADATA(IID "com.geometryfactory.CGALLab.PluginInterface/1.0")

private:
  QAction* actionOffsetMeshing;
  QAction* actionInflateMesh;

  Scene_interface *scene;
  QMainWindow *mw;

public:
  void init(QMainWindow* mainWindow,
            Scene_interface* scene_interface,
            Messages_interface*)
  {
    this->scene = scene_interface;
    this->mw = mainWindow;

    actionOffsetMeshing = new QAction(tr("Offset Meshing"), mw);
    actionOffsetMeshing->setProperty("subMenuName", "3D Surface Mesh Generation");
    connect(actionOffsetMeshing, SIGNAL(triggered()),
            this, SLOT(offset_meshing()));

    actionInflateMesh = new QAction(tr("Inflate Mesh"), mw);
    actionInflateMesh->setProperty("subMenuName", "Operations on Polyhedra");
    connect(actionInflateMesh, SIGNAL(triggered()),
            this, SLOT(inflate_mesh()));
  }

  bool applicable(QAction* action) const
  {
    if(action == actionOffsetMeshing)
    {
      if(scene->selectionIndices().size() == 1)
      {
        const int index = scene->mainSelectionIndex();
        return (qobject_cast<Scene_surface_mesh_item*>(scene->item(index)) ||
                qobject_cast<Scene_polygon_soup_item*>(scene->item(index)));
      }

      // Can provide a polyline item for feature protection
      if(scene->selectionIndices().size() != 2)
        return false;

      // One needs to be a surface mesh or polygon soup item, and the other a polyline item
      const int index1 = scene->selectionIndices().at(0);
      const int index2 = scene->selectionIndices().at(1);
      Scene_item* item1 = scene->item(index1);
      Scene_item* item2 = scene->item(index2);

      if((qobject_cast<Scene_surface_mesh_item*>(item1) ||
          qobject_cast<Scene_polygon_soup_item*>(item1)) &&
         qobject_cast<Scene_polylines_item*>(item2))
        return true;

      if((qobject_cast<Scene_surface_mesh_item*>(item2) ||
          qobject_cast<Scene_polygon_soup_item*>(item2)) &&
         qobject_cast<Scene_polylines_item*>(item1))
        return true;
    }
    else if(action == actionInflateMesh)
    {
      if(scene->selectionIndices().size() == 1)
      {
        const int index = scene->mainSelectionIndex();
        return qobject_cast<Scene_surface_mesh_item*>(scene->item(index));
      }
    }

    return false;
  }

  QList<QAction*> actions() const
  {
    return QList<QAction*>() << actionOffsetMeshing
                             << actionInflateMesh;
  }

public Q_SLOTS:
  void offset_meshing();
  void inflate_mesh();
}; // class CGAL_Lab_offset_meshing_plugin

void
CGAL_Lab_offset_meshing_plugin::
offset_meshing()
{
  Scene_item* item = nullptr;
  Scene_surface_mesh_item* sm_item = nullptr;
  Scene_polygon_soup_item* soup_item = nullptr;
  Scene_polylines_item* polylines_item = nullptr;

  bool mesh_or_soup_item_found = false;
  for(Scene_interface::Item_id index : scene->selectionIndices())
  {
    if(!mesh_or_soup_item_found)
    {
      sm_item = qobject_cast<Scene_surface_mesh_item*>(scene->item(index));
      if(sm_item == nullptr)
      {
        soup_item = qobject_cast<Scene_polygon_soup_item*>(scene->item(index));
        if(soup_item != nullptr)
        {
          item = scene->item(index);
          mesh_or_soup_item_found = true;
          continue;
        }
      }
      else
      {
        item = scene->item(index);
        mesh_or_soup_item_found = true;
        continue;
      }
    }

    polylines_item = qobject_cast<Scene_polylines_item*>(scene->item(index));
  }

  QApplication::setOverrideCursor(Qt::WaitCursor);

  if(!mesh_or_soup_item_found)
    return;

  if(sm_item)
  {
    if(!is_triangle_mesh(*(sm_item->face_graph())))
    {
      QMessageBox::critical(mw,
                            tr("Offset Meshing"),
                            tr("The selected mesh is not a triangle mesh."));
      return;
    }
  }
  else
  {
    for(const auto& p : soup_item->polygons())
    {
      if(p.size() != 3)
      {
        QMessageBox::critical(mw,
                              tr("Offset Meshing"),
                              tr("The selected polygon soup is not a triangle soup."));
        return;
      }
    }
  }

  double diag;
  if(sm_item)
    diag = sm_item->bboxDiagonal();
  else
    diag = soup_item->bboxDiagonal();

  QApplication::restoreOverrideCursor();

  bool ok = true;
  double offset_value = QInputDialog::getDouble(mw,
                                                QString("Choose Offset Value"),
                                                QString("Offset Value (use a negative number to compute the inset of a closed mesh)"),
                                                0.1 * diag,
                                                - (std::numeric_limits<double>::max)(),
                                                (std::numeric_limits<double>::max)(), 10, &ok);
  if(!ok)
    return;

  if(offset_value < 0 && (!sm_item || !is_closed(*(sm_item->face_graph()))))
  {
    QMessageBox::critical(mw,
                          tr("Offset Meshing"),
                          tr("Insetting is only possible for closed polygon meshes."));
    return;
  }

  QDialog dialog(mw);
  Ui::Offset_meshing_dialog ui;
  ui.setupUi(&dialog);
  ui.angle->setRange(1.0, 30.0);

  connect(ui.buttonBox, SIGNAL(accepted()),
          &dialog, SLOT(accept()));
  connect(ui.buttonBox, SIGNAL(rejected()),
          &dialog, SLOT(reject()));

  ui.sizing->setRange(diag * 10e-6, diag);
  ui.sizing->setValue(diag * 0.05); // default value
  ui.approx->setRange(diag * 10e-7, diag);
  ui.approx->setValue(diag * 0.005);

  if(polylines_item != nullptr)
  {
    ui.edge_sizing->setRange(diag * 10e-6, diag);
    ui.edge_sizing->setValue(diag * 0.05); // default value
  }
  else
  {
    ui.edge_sizing->setEnabled(false);
  }

  int i = dialog.exec();
  if(i == QDialog::Rejected)
    return;

  const double angle = ui.angle->value();
  const double approx = ui.approx->value();
  const double sizing = ui.sizing->value();
  const double edge_size = (polylines_item != nullptr) ? ui.edge_sizing->value() : 0;
  const int tag_index = ui.tags->currentIndex();

  if(tag_index < 0)
    return;

  QApplication::setOverrideCursor(Qt::BusyCursor);

  std::cerr << "mesh with:"
            << "\n  angle= " << angle
            << "\n  sizing= " << sizing
            << "\n  approx= " << approx
            << "\n  tag= " << tag_index
            << std::boolalpha
            << std::endl;

  Mesher_thread* worker = nullptr;
  if(soup_item)
  {
    worker = new Mesher_thread(nullptr, soup_item, polylines_item,
                               offset_value,
                               angle, sizing, approx, edge_size, tag_index);
  }
  else
  {
    worker = new Mesher_thread(sm_item, nullptr, polylines_item,
                               offset_value,
                               angle, sizing, approx, edge_size, tag_index);
  }

  connect(worker, &QThread::finished,
          worker, &QObject::deleteLater);

  connect(worker, &Mesher_thread::resultReady,
          this, [item, angle, sizing, approx, offset_value/* , index */](SMesh* offset_mesh)
                {
                  if(!offset_mesh)
                  {
                    QApplication::restoreOverrideCursor();

                    Three::getMutex()->lock();
                    Three::isLocked() = false;
                    Three::getMutex()->unlock();

                    return;
                  }

                  Scene_surface_mesh_item* offset_item = new Scene_surface_mesh_item(offset_mesh);
                  offset_item->setName(tr("%1 offset %5 (%2 %3 %4)").arg(item->name())
                                                                   .arg(angle)
                                                                   .arg(sizing)
                                                                   .arg(approx)
                                                                   .arg(offset_value));
                  offset_item->setColor(Qt::magenta);
                  offset_item->setRenderingMode(Wireframe);
                  Three::scene()->addItem(offset_item);

                  QApplication::restoreOverrideCursor();

                  Three::getMutex()->lock();
                  Three::isLocked() = false;
                  Three::getMutex()->unlock();
                });

  QMessageBox* message_box = new QMessageBox(QMessageBox::NoIcon,
                                             "Meshing",
                                             "Offset meshing in progress...",
                                             QMessageBox::Cancel,
                                             mw);
  message_box->setDefaultButton(QMessageBox::Cancel);
  QAbstractButton* cancelButton = message_box->button(QMessageBox::Cancel);
  cancelButton->setText(tr("Stop"));

  connect(cancelButton, &QAbstractButton::clicked,
          this, [worker](){ worker->terminate(); });
  connect(worker, &Mesher_thread::finished,
          message_box, &QMessageBox::close);

  Three::getMutex()->lock();
  Three::isLocked() = true;
  Three::getMutex()->unlock();

  message_box->open();
  worker->start();
}

void
CGAL_Lab_offset_meshing_plugin::
inflate_mesh()
{
  const Scene_interface::Item_id index = scene->mainSelectionIndex();
  Scene_item* item = scene->item(index);
  if(item == nullptr)
    return;

  Scene_surface_mesh_item* sm_item = qobject_cast<Scene_surface_mesh_item*>(item);
  if(sm_item == nullptr)
    return;

  SMesh* sMesh = sm_item->face_graph();
  if(sMesh == nullptr)
    return;

  const double diag = sm_item->bboxDiagonal();

  bool ok = true;
  const double offset_value = QInputDialog::getDouble(mw,
                                                      QString("Choose Inflate Distance"),
                                                      QString("Inflate Distance (use a negative number to deflate)"),
                                                      0.1 * diag,
                                                      -(std::numeric_limits<double>::max)(),
                                                      (std::numeric_limits<double>::max)(),
                                                      10,
                                                      &ok);
  if(!ok)
    return;

  auto vpm = get(CGAL::vertex_point, *sMesh);
  auto vnm = sMesh->property_map<vertex_descriptor, EPICK::Vector_3 >("v:normal").value();

  QApplication::setOverrideCursor(Qt::WaitCursor);

  for(const auto& v : vertices(*sMesh))
  {
    const EPICK::Vector_3& n = get(vnm, v);
    put(vpm, v, get(vpm, v) + offset_value * n);
  }

  sm_item->invalidateOpenGLBuffers();
  sm_item->itemChanged();
  sm_item->itemVisibilityChanged();

  QApplication::restoreOverrideCursor();
}

#include "Offset_meshing_plugin.moc"
