// Gmsh - Copyright (C) 1997-2019 C. Geuzaine, J.-F. Remacle
//
// See the LICENSE.txt file for license information. Please report all
// issues on https://gitlab.onelab.info/gmsh/gmsh/issues.

#ifndef OCC_ATTRIBUTES_H
#define OCC_ATTRIBUTES_H

#include <vector>
#include <string>
#include "GmshConfig.h"
#include "GmshMessage.h"
#include "OS.h"
#include "Context.h"
#include "rtree.h"

#if defined(HAVE_OCC)

#include <Bnd_Box.hxx>
#include <BRepBndLib.hxx>
#include <TopoDS_Shape.hxx>
#include <BRepTools.hxx>

class OCCAttributes {
private:
  int _dim;
  TopoDS_Shape _shape;
  double _meshSize;
  ExtrudeParams *_extrude;
  int _sourceDim;
  TopoDS_Shape _sourceShape;
  std::string _label;
  std::vector<double> _color;
public:
  OCCAttributes()
    : _dim(-1), _meshSize(MAX_LC), _extrude(0), _sourceDim(-1)
  {
  }
  OCCAttributes(int dim, TopoDS_Shape shape)
    : _dim(dim), _shape(shape), _meshSize(MAX_LC), _extrude(0), _sourceDim(-1)
  {
  }
  OCCAttributes(int dim, TopoDS_Shape shape, double size)
    : _dim(dim), _shape(shape), _meshSize(size), _extrude(0), _sourceDim(-1)
  {
  }
  OCCAttributes(int dim, TopoDS_Shape shape, ExtrudeParams *e,
                    int sourceDim, TopoDS_Shape sourceShape)
    : _dim(dim), _shape(shape), _meshSize(MAX_LC), _extrude(e),
      _sourceDim(sourceDim), _sourceShape(sourceShape)
  {
  }
  OCCAttributes(int dim, TopoDS_Shape shape, const std::string &label)
    : _dim(dim), _shape(shape), _meshSize(MAX_LC), _extrude(0), _sourceDim(-1),
      _label(label)
  {
  }
  OCCAttributes(int dim, TopoDS_Shape shape, double r, double g, double b,
                double a = 1., int boundary = 0)
    : _dim(dim), _shape(shape), _meshSize(MAX_LC), _extrude(0), _sourceDim(-1)
  {
    _color.resize(boundary ? 5 : 4);
    _color[0] = r;
    _color[1] = g;
    _color[2] = b;
    _color[3] = a;
    if(boundary) _color[4] = boundary;
  }
  ~OCCAttributes() {}
  int getDim() { return _dim; }
  TopoDS_Shape getShape() { return _shape; }
  double getMeshSize() { return _meshSize; }
  ExtrudeParams *getExtrudeParams() { return _extrude; }
  int getSourceDim() { return _sourceDim; }
  TopoDS_Shape getSourceShape() { return _sourceShape; }
  const std::string &getLabel() { return _label; }
  const std::vector<double> &getColor() { return _color; }
};

// attributes are stored according to the center of their associated shape
// bounding box; this allows to efficiently search for potential matches, even
// if the actual underlying shape has been modified (typically through boolean
// fragments)
class OCCAttributesRTree {
private:
  RTree<OCCAttributes *, double, 3, double> *_rtree[4];
  std::vector<OCCAttributes *> _all;
  double _tol;
  static bool rtree_callback(OCCAttributes *v, void *ctx)
  {
    std::vector<OCCAttributes *> *out =
      static_cast<std::vector<OCCAttributes *> *>(ctx);
    out->push_back(v);
    return true;
  }
  void _find(int dim, const TopoDS_Shape &shape,
             std::vector<OCCAttributes *> &attr,
             bool requireMeshSize, bool requireExtrudeParams, bool requireLabel,
             bool requireColor, bool excludeSame)
  {
    attr.clear();
    if(dim < 0 || dim > 3) return;
    Bnd_Box box;
    try {
      BRepBndLib::Add(shape, box);
      if(box.IsVoid()) {
        Msg::Debug(
          "Searching for (null or degenerate) shape with void bounding box");
        return;
      }
    } catch(Standard_Failure &err) {
      Msg::Error("OpenCASCADE exception %s", err.GetMessageString());
      return;
    }
    double xmin, ymin, zmin, xmax, ymax, zmax;
    box.Get(xmin, ymin, zmin, xmax, ymax, zmax);
    double x = 0.5 * (xmin + xmax);
    double y = 0.5 * (ymin + ymax);
    double z = 0.5 * (zmin + zmax);
    double bmin[3] = {x - _tol, y - _tol, z - _tol};
    double bmax[3] = {x + _tol, y + _tol, z + _tol};
    std::vector<OCCAttributes *> tmp;
    _rtree[dim]->Search(bmin, bmax, rtree_callback, &tmp);
    Msg::Debug("OCCRTree found %d matches at (%g,%g,%g) in tree of size %d",
               (int)tmp.size(), x, y, z, (int)_all.size());
    if(tmp.empty()) { // no match
      return;
    }
    if(!excludeSame) {
      for(std::size_t i = 0; i < tmp.size(); i++) {
        if(requireMeshSize && tmp[i]->getMeshSize() == MAX_LC) continue;
        if(requireExtrudeParams && !tmp[i]->getExtrudeParams()) continue;
        if(requireLabel && tmp[i]->getLabel().empty()) continue;
        if(requireColor && tmp[i]->getColor().empty()) continue;
        if(shape.IsSame(tmp[i]->getShape())) { // exact match: same shape
          attr.push_back(tmp[i]);
          Msg::Debug("OCCRTree exact match");
          return;
        }
      }
    }
    // potential matches based on bounding box
    for(std::size_t i = 0; i < tmp.size(); i++) {
      if(requireMeshSize && tmp[i]->getMeshSize() == MAX_LC) continue;
      if(requireExtrudeParams && !tmp[i]->getExtrudeParams()) continue;
      if(requireLabel && tmp[i]->getLabel().empty()) continue;
      if(requireColor && tmp[i]->getColor().empty()) continue;
      Bnd_Box box2;
      BRepBndLib::Add(tmp[i]->getShape(), box2);
      if(box.Distance(box2) < _tol) {
        attr.push_back(tmp[i]);
      }
    }
    Msg::Debug("OCCRtree %d matches after bounding box filtering",
               (int)attr.size());
  }

public:
  OCCAttributesRTree(double tolerance = 1.e-8)
  {
    for(int dim = 0; dim < 4; dim++)
      _rtree[dim] = new RTree<OCCAttributes *, double, 3, double>();
    _tol = tolerance;
  }
  ~OCCAttributesRTree()
  {
    clear();
    for(int dim = 0; dim < 4; dim++) delete _rtree[dim];
  }
  void clear()
  {
    for(int dim = 0; dim < 4; dim++) _rtree[dim]->RemoveAll();
    for(std::size_t i = 0; i < _all.size(); i++) delete _all[i];
    _all.clear();
  }
  void print(const std::string &fileName = "")
  {
    FILE *fp = stdout;
    if(fileName.size()) {
      fp = Fopen(fileName.c_str(), "w");
      if(!fp) {
        Msg::Error("Could not open file '%s'", fileName.c_str());
        return;
      }
    }
    fprintf(fp, "View(\"rtree mesh sizes\"){\n");
    for(std::size_t i = 0; i < _all.size(); i++) {
      if(_all[i]->getDim() != 0) continue;
      if(_all[i]->getMeshSize() == MAX_LC) continue;
      gp_Pnt pnt = BRep_Tool::Pnt(TopoDS::Vertex(_all[i]->getShape()));
      fprintf(fp, "SP(%g,%g,%g){%g};\n", pnt.X(), pnt.Y(), pnt.Z(),
              _all[i]->getMeshSize());
    }
    fprintf(fp, "};\n");
    if(fileName.size()) fclose(fp);
  }
  void insert(OCCAttributes *v)
  {
    _all.push_back(v);
    if(v->getDim() < 0 || v->getDim() > 3) return;
    Bnd_Box box;
    try {
      BRepBndLib::Add(v->getShape(), box);
      if(box.IsVoid()) {
        Msg::Debug("Inserting (null or degenerate) shape with void bounding box");
        // BRepTools::Dump(v->getShape(), std::cout);
        return;
      }
    } catch(Standard_Failure &err) {
      Msg::Error("OpenCASCADE exception %s", err.GetMessageString());
      return;
    }
    double xmin, ymin, zmin, xmax, ymax, zmax;
    box.Get(xmin, ymin, zmin, xmax, ymax, zmax);
    double x = 0.5 * (xmin + xmax);
    double y = 0.5 * (ymin + ymax);
    double z = 0.5 * (zmin + zmax);
    double bmin[3] = {x - _tol, y - _tol, z - _tol};
    double bmax[3] = {x + _tol, y + _tol, z + _tol};
    _rtree[v->getDim()]->Insert(bmin, bmax, v);
  }
  void remove(OCCAttributes *v)
  {
    if(v->getDim() < 0 || v->getDim() > 3) return;
    Bnd_Box box;
    try {
      BRepBndLib::Add(v->getShape(), box);
      if(box.IsVoid()) {
        Msg::Debug("Removing (null or degenerate) shape with void bounding box");
        // BRepTools::Dump(v->getShape(), std::cout);
        return;
      }
    } catch(Standard_Failure &err) {
      Msg::Error("OpenCASCADE exception %s", err.GetMessageString());
      return;
    }
    double xmin, ymin, zmin, xmax, ymax, zmax;
    box.Get(xmin, ymin, zmin, xmax, ymax, zmax);
    double x = 0.5 * (xmin + xmax);
    double y = 0.5 * (ymin + ymax);
    double z = 0.5 * (zmin + zmax);
    double bmin[3] = {x - _tol, y - _tol, z - _tol};
    double bmax[3] = {x + _tol, y + _tol, z + _tol};
    _rtree[v->getDim()]->Remove(bmin, bmax, v);
  }
  double getMeshSize(int dim, TopoDS_Shape shape)
  {
    std::vector<OCCAttributes *> attr;
    _find(dim, shape, attr, true, false, false, false, false);
    for(std::size_t i = 0; i < attr.size(); i++) {
      if(attr[i]->getMeshSize() < MAX_LC) return attr[i]->getMeshSize();
    }
    return MAX_LC;
  }
  ExtrudeParams *getExtrudeParams(int dim, TopoDS_Shape shape, int &sourceDim,
                                  TopoDS_Shape &sourceShape)
  {
    std::vector<OCCAttributes *> attr;
    _find(dim, shape, attr, false, true, false, false, false);
    for(std::size_t i = 0; i < attr.size(); i++) {
      if(attr[i]->getExtrudeParams()) {
        sourceDim = attr[i]->getSourceDim();
        sourceShape = attr[i]->getSourceShape();
        return attr[i]->getExtrudeParams();
      }
    }
    return 0;
  }
  void getLabels(int dim, TopoDS_Shape shape, std::vector<std::string> &labels)
  {
    labels.clear();
    std::vector<OCCAttributes *> attr;
    _find(dim, shape, attr, false, false, true, false, false);
    for(std::size_t i = 0; i < attr.size(); i++) {
      if(!attr[i]->getLabel().empty())
        labels.push_back(attr[i]->getLabel());
    }
  }
  bool getColor(int dim, TopoDS_Shape shape, unsigned int &color,
                unsigned int &boundary)
  {
    std::vector<OCCAttributes *> attr;
    _find(dim, shape, attr, false, false, false, true, false);
    for(std::size_t i = 0; i < attr.size(); i++) {
      const std::vector<double> &col = attr[i]->getColor();
      if(col.size() >= 3){
        int r = (int)(col[0] * 255);
        r = (r < 0) ? 0 : (r > 255) ? 255 : r;
        int g = (int)(col[1] * 255);
        g = (g < 0) ? 0 : (g > 255) ? 255 : g;
        int b = (int)(col[2] * 255);
        b = (b < 0) ? 0 : (b > 255) ? 255 : b;
        int a = 255;
        if(col.size() >= 4){
          int a = (int)(col[3] * 255);
          a = (a < 0) ? 0 : (a > 255) ? 255 : a;
        }
        color = CTX::instance()->packColor(r, b, g, a);
        boundary = (col.size() == 5) ? col[4] : 0;
        return true;
      }
    }
    return false;
  }
  void getSimilarShapes(int dim, TopoDS_Shape shape,
                        std::vector<TopoDS_Shape> &other)
  {
    std::vector<OCCAttributes *> attr;
    _find(dim, shape, attr, false, false, false, false, true);
    for(std::size_t i = 0; i < attr.size(); i++) {
      TopoDS_Shape s = attr[i]->getShape();
      if(!s.IsNull()) other.push_back(s);
    }
  }
};

#endif
#endif
