/* SPDX-FileCopyrightText: 2023 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

#pragma once

/** \file
 * \ingroup freestyle
 * \brief Class to define a cell grid surrounding the projected image of a scene
 */

#define SPHERICAL_GRID_LOGGING 0

// I would like to avoid using deque because including ViewMap.h and <deque> or <vector> separately
// results in redefinitions of identifiers. ViewMap.h already includes <vector> so it should be a
// safe fall-back.
// #include <vector>
// #include <deque>

#include "GridDensityProvider.h"
#include "OccluderSource.h"
#include "ViewMap.h"

#include "../geometry/BBox.h"
#include "../geometry/GridHelpers.h"
#include "../geometry/Polygon.h"

#include "../system/PointerSequence.h"

#include "../winged_edge/WEdge.h"

#include "BKE_global.hh"

#include "MEM_guardedalloc.h"

namespace Freestyle {

class SphericalGrid {
 public:
  // Helper classes
  struct OccluderData {
    explicit OccluderData(OccluderSource &source, Polygon3r &p);
    Polygon3r poly;
    Polygon3r cameraSpacePolygon;
    real shallowest, deepest;
    // N.B. We could, of course, store face in poly's userdata member, like the old ViewMapBuilder
    // code does. However, code comments make it clear that userdata is deprecated, so we avoid the
    // temptation to save 4 or 8 bytes.
    WFace *face;

    MEM_CXX_CLASS_ALLOC_FUNCS("Freestyle:SphericalGrid:OccluderData")
  };

 private:
  struct Cell {
    // Can't store Cell in a vector without copy and assign
    // Cell(const Cell& other);
    // Cell& operator=(const Cell& other);

    explicit Cell() = default;

    static bool compareOccludersByShallowestPoint(const OccluderData *a, const OccluderData *b);

    void setDimensions(real x, real y, real sizeX, real sizeY);
    void checkAndInsert(OccluderSource &source, Polygon3r &poly, OccluderData *&occluder);
    void indexPolygons();

    real boundary[4];
    // deque<OccluderData*> faces;
    vector<OccluderData *> faces;
  };

 public:
  /**
   * Iterator needs to allow the user to avoid full 3D comparison in two cases:
   *
   *  (1) Where (*current)->deepest < target[2], where the occluder is unambiguously in front of
   * the target point.
   *
   *  (2) Where (*current)->shallowest > target[2], where the occluder is unambiguously in back of
   * the target point.
   *
   *  In addition, when used by OptimizedFindOccludee, Iterator should stop iterating as soon as it
   * has an occludee candidate and (*current)->shallowest > candidate[2], because at that point
   * forward no new occluder could possibly be a better occludee.
   */

  class Iterator {
   public:
    // epsilon is not used in this class, but other grids with the same interface may need an
    // epsilon
    explicit Iterator(SphericalGrid &grid, Vec3r &center, real epsilon = 1.0e-06);
    void initBeforeTarget();
    void initAfterTarget();
    void nextOccluder();
    void nextOccludee();
    bool validBeforeTarget();
    bool validAfterTarget();
    WFace *getWFace() const;
    Polygon3r *getCameraSpacePolygon();
    void reportDepth(Vec3r origin, Vec3r u, real t);

   private:
    bool testOccluder(bool wantOccludee);
    void markCurrentOccludeeCandidate(real depth);

    Cell *_cell;
    Vec3r _target;
    bool _foundOccludee;
    real _occludeeDepth;
    // deque<OccluderData*>::iterator _current, _occludeeCandidate;
    vector<OccluderData *>::iterator _current, _occludeeCandidate;

    MEM_CXX_CLASS_ALLOC_FUNCS("Freestyle:SphericalGrid:Iterator")
  };

  class Transform : public GridHelpers::Transform {
   public:
    explicit Transform() = default;
    explicit Transform(Transform &other);
    Vec3r operator()(const Vec3r &point) const;
    static Vec3r sphericalProjection(const Vec3r &M);
  };

 private:
  // Prevent implicit copies and assignments.
  SphericalGrid(const SphericalGrid &other);
  SphericalGrid &operator=(const SphericalGrid &other);

 public:
  explicit SphericalGrid(OccluderSource &source,
                         GridDensityProvider &density,
                         ViewMap *viewMap,
                         Vec3r &viewpoint,
                         bool enableQI);
  virtual ~SphericalGrid();

  // Generate Cell structure
  void assignCells(OccluderSource &source, GridDensityProvider &density, ViewMap *viewMap);
  // Fill Cells
  void distributePolygons(OccluderSource &source);
  // Insert one polygon into each matching cell, return true if any cell consumes the polygon
  bool insertOccluder(OccluderSource &source, OccluderData *&occluder);
  // Sort occluders in each cell
  void reorganizeCells();

  Cell *findCell(const Vec3r &point);

  // Accessors:
  bool orthographicProjection() const;
  const Vec3r &viewpoint() const;
  bool enableQI() const;

 private:
  void getCellCoordinates(const Vec3r &point, uint &x, uint &y);

  typedef PointerSequence<vector<Cell *>, Cell *> cellContainer;
  // typedef PointerSequence<deque<OccluderData*>, OccluderData*> occluderContainer;
  typedef PointerSequence<vector<OccluderData *>, OccluderData *> occluderContainer;
  uint _cellsX, _cellsY;
  float _cellSize;
  float _cellOrigin[2];
  cellContainer _cells;
  occluderContainer _faces;
  Vec3r _viewpoint;
  bool _enableQI;

  MEM_CXX_CLASS_ALLOC_FUNCS("Freestyle:SphericalGrid")
};

inline void SphericalGrid::Iterator::initBeforeTarget()
{
  _current = _cell->faces.begin();
  while (_current != _cell->faces.end() && !testOccluder(false)) {
    ++_current;
  }
}

inline void SphericalGrid::Iterator::initAfterTarget()
{
  if (_foundOccludee) {
#if SPHERICAL_GRID_LOGGING
    if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
      std::cout << "\tStarting occludee search from occludeeCandidate at depth " << _occludeeDepth
                << std::endl;
    }
#endif
    _current = _occludeeCandidate;
    return;
  }

#if SPHERICAL_GRID_LOGGING
  if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
    std::cout << "\tStarting occludee search from current position" << std::endl;
  }
#endif

  while (_current != _cell->faces.end() && !testOccluder(true)) {
    ++_current;
  }
}

inline bool SphericalGrid::Iterator::testOccluder(bool wantOccludee)
{
  // End-of-list is not even a valid iterator position
  if (_current == _cell->faces.end()) {
    // Returning true seems strange, but it will break us out of whatever loop is calling
    // testOccluder, and _current=_cell->face.end() will make the calling routine give up.
    return true;
  }
#if SPHERICAL_GRID_LOGGING
  if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
    std::cout << "\tTesting occluder " << (*_current)->poly.getVertices()[0];
    for (uint i = 1; i < (*_current)->poly.getVertices().size(); ++i) {
      std::cout << ", " << (*_current)->poly.getVertices()[i];
    }
    std::cout << " from shape " << (*_current)->face->GetVertex(0)->shape()->GetId() << std::endl;
  }
#endif

  // If we have an occluder candidate and we are unambiguously after it, abort
  if (_foundOccludee && (*_current)->shallowest > _occludeeDepth) {
#if SPHERICAL_GRID_LOGGING
    if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
      std::cout << "\t\tAborting: shallowest > occludeeCandidate->deepest" << std::endl;
    }
#endif
    _current = _cell->faces.end();

    // See note above
    return true;
  }

  // Specific continue or stop conditions when searching for each type
  if (wantOccludee) {
    if ((*_current)->deepest < _target[2]) {
#if SPHERICAL_GRID_LOGGING
      if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
        std::cout << "\t\tSkipping: shallower than target while looking for occludee" << std::endl;
      }
#endif
      return false;
    }
  }
  else {
    if ((*_current)->shallowest > _target[2]) {
#if SPHERICAL_GRID_LOGGING
      if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
        std::cout << "\t\tStopping: deeper than target while looking for occluder" << std::endl;
      }
#endif
      return true;
    }
  }

  // Depthwise, this is a valid occluder.

  // Check to see if target is in the 2D bounding box
  Vec3r bbMin, bbMax;
  (*_current)->poly.getBBox(bbMin, bbMax);
  if (_target[0] < bbMin[0] || _target[0] > bbMax[0] || _target[1] < bbMin[1] ||
      _target[1] > bbMax[1])
  {
#if SPHERICAL_GRID_LOGGING
    if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
      std::cout << "\t\tSkipping: bounding box violation" << std::endl;
    }
#endif
    return false;
  }

  // We've done all the corner cutting we can. Let the caller work out whether or not the geometry
  // is correct.
  return true;
}

inline void SphericalGrid::Iterator::reportDepth(Vec3r origin, Vec3r u, real t)
{
  // The reported depth is the length of a ray in camera space. We need to convert it into the
  // distance from viewpoint If origin is the viewpoint, depth == t. A future optimization could
  // allow the caller to tell us if origin is viewponit or target, at the cost of changing the
  // OptimizedGrid API.
  real depth = (origin + u * t).norm();
#if SPHERICAL_GRID_LOGGING
  if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
    std::cout << "\t\tReporting depth of occluder/ee: " << depth;
  }
#endif
  if (depth > _target[2]) {
#if SPHERICAL_GRID_LOGGING
    if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
      std::cout << " is deeper than target" << std::endl;
    }
#endif
    // If the current occluder is the best occludee so far, save it.
    if (!_foundOccludee || _occludeeDepth > depth) {
      markCurrentOccludeeCandidate(depth);
    }
  }
  else {
#if SPHERICAL_GRID_LOGGING
    if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
      std::cout << std::endl;
    }
#endif
  }
}

inline void SphericalGrid::Iterator::nextOccluder()
{
  if (_current != _cell->faces.end()) {
    do {
      ++_current;
    } while (_current != _cell->faces.end() && !testOccluder(false));
  }
}

inline void SphericalGrid::Iterator::nextOccludee()
{
  if (_current != _cell->faces.end()) {
    do {
      ++_current;
    } while (_current != _cell->faces.end() && !testOccluder(true));
  }
}

inline bool SphericalGrid::Iterator::validBeforeTarget()
{
  return _current != _cell->faces.end() && (*_current)->shallowest <= _target[2];
}

inline bool SphericalGrid::Iterator::validAfterTarget()
{
  return _current != _cell->faces.end();
}

inline void SphericalGrid::Iterator::markCurrentOccludeeCandidate(real depth)
{
#if SPHERICAL_GRID_LOGGING
  if (blender::G.debug & blender::G_DEBUG_FREESTYLE) {
    std::cout << "\t\tFound occludeeCandidate at depth " << depth << std::endl;
  }
#endif
  _occludeeCandidate = _current;
  _occludeeDepth = depth;
  _foundOccludee = true;
}

inline WFace *SphericalGrid::Iterator::getWFace() const
{
  return (*_current)->face;
}

inline Polygon3r *SphericalGrid::Iterator::getCameraSpacePolygon()
{
  return &((*_current)->cameraSpacePolygon);
}

inline SphericalGrid::OccluderData::OccluderData(OccluderSource &source, Polygon3r &p)
    : poly(p), cameraSpacePolygon(source.getCameraSpacePolygon()), face(source.getWFace())
{
  const Vec3r viewpoint(0, 0, 0);
  // Get the point on the camera-space polygon that is closest to the viewpoint
  // shallowest is the distance from the viewpoint to that point
  shallowest = GridHelpers::distancePointToPolygon(viewpoint, cameraSpacePolygon);

  // Get the point on the camera-space polygon that is furthest from the viewpoint
  // deepest is the distance from the viewpoint to that point
  deepest = cameraSpacePolygon.getVertices()[2].norm();
  for (uint i = 0; i < 2; ++i) {
    real t = cameraSpacePolygon.getVertices()[i].norm();
    if (t > deepest) {
      deepest = t;
    }
  }
}

inline void SphericalGrid::Cell::checkAndInsert(OccluderSource &source,
                                                Polygon3r &poly,
                                                OccluderData *&occluder)
{
  if (GridHelpers::insideProscenium(boundary, poly)) {
    if (occluder == nullptr) {
      // Disposal of occluder will be handled in SphericalGrid::distributePolygons(),
      // or automatically by SphericalGrid::_faces;
      occluder = new OccluderData(source, poly);
    }
    faces.push_back(occluder);
  }
}

inline bool SphericalGrid::insertOccluder(OccluderSource &source, OccluderData *&occluder)
{
  Polygon3r &poly(source.getGridSpacePolygon());
  occluder = nullptr;

  Vec3r bbMin, bbMax;
  poly.getBBox(bbMin, bbMax);
  // Check overlapping cells
  uint startX, startY, endX, endY;
  getCellCoordinates(bbMin, startX, startY);
  getCellCoordinates(bbMax, endX, endY);

  for (uint i = startX; i <= endX; ++i) {
    for (uint j = startY; j <= endY; ++j) {
      if (_cells[i * _cellsY + j] != nullptr) {
        _cells[i * _cellsY + j]->checkAndInsert(source, poly, occluder);
      }
    }
  }

  return occluder != nullptr;
}

} /* namespace Freestyle */
