#pragma once

#include <Core/RaCore.hpp>

#include <Core/Containers/VectorArray.hpp>
#include <Core/Geometry/OpenMesh.hpp>
#include <Core/Geometry/TriangleMesh.hpp>
#include <Core/Types.hpp>
#include <Core/Utils/Index.hpp>
#include <Core/Utils/StdOptional.hpp>

#include <OpenMesh/Core/Mesh/PolyMesh_ArrayKernelT.hh>
#include <OpenMesh/Core/Mesh/Traits.hh>
#include <OpenMesh/Core/Mesh/TriMesh_ArrayKernelT.hh>
#include <OpenMesh/Core/Utils/Property.hh>
#include <OpenMesh/Core/Utils/PropertyManager.hh>

#include <Eigen/Core>
#include <Eigen/Geometry>

#include <set>

namespace Ra {
namespace Core {
namespace Geometry {
using namespace Utils; // log, AttribXXX

/**
 * Define the Traits to be used by OpenMesh for TopologicalMesh.
 */
struct TopologicalMeshTraits : OpenMesh::DefaultTraits {
    using Point  = Ra::Core::Vector3;
    using Normal = Ra::Core::Vector3;

    VertexAttributes( OpenMesh::Attributes::Status | OpenMesh::Attributes::Normal );
    FaceAttributes( OpenMesh::Attributes::Status | OpenMesh::Attributes::Normal );
    EdgeAttributes( OpenMesh::Attributes::Status );
    // Add  OpenMesh::Attributes::PrevHalfedge for efficiency ?
    HalfedgeAttributes( OpenMesh::Attributes::Status | OpenMesh::Attributes::Normal );
};

/**
 * This class represents a mesh with topological information on the
 * vertex graph, using a half-edge representation.
 *
 * This integration is inspired by:
 * https://gist.github.com/Unril/03fa353d0461ed6bd41d
 *
 * \todo rename methods to respect Radium guideline (get/set/is, camelCase)
 * \todo private inheritance from OpenMesh, and import relevant methods.
 */
class RA_CORE_API TopologicalMesh : public OpenMesh::PolyMesh_ArrayKernelT<TopologicalMeshTraits>
{
  private:
    using base = OpenMesh::PolyMesh_ArrayKernelT<TopologicalMeshTraits>;
    using base::PolyMesh_ArrayKernelT;
    using Vector3 = Ra::Core::Vector3;
    using Index   = Ra::Core::Utils::Index;
    class Wedge;
    class WedgeCollection;

  public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW

    class WedgeData;
    using WedgeIndex = Ra::Core::Utils::Index;

    /**
     * Construct an empty topological mesh, only initialize mandatory properties.
     */
    explicit TopologicalMesh();

    /**
     * \brief Convenience constructor
     * \see TopologicalMesh( const Ra::Core::Geometry::TriangleMesh&, NonManifoldFaceCommand)
     */
    template <typename MeshIndex>
    explicit TopologicalMesh( [[deprecated(
        "Use MultiIndexedGeometry instead" )]] const Ra::Core::Geometry::IndexedGeometry<MeshIndex>&
                                  mesh );

    /**
     * \brief Convenience constructor
     * \see TopologicalMesh( const Ra::Core::Geometry::TriangleMesh&, NonManifoldFaceCommand)
     * @todo, when MultiIndexedGeometry will be operational, will this replace the above ?
     */
    explicit TopologicalMesh(
        const Ra::Core::Geometry::MultiIndexedGeometry& mesh,
        const Ra::Core::Geometry::MultiIndexedGeometry::LayerKeyType& layerKey );

    /**
     * Construct a topological mesh from a triangle mesh.
     * This operation merges vertex with same position, but keeps vertex
     * attributes on halfedges, so that TriangleMesh vertices with the same 3D
     * position are represented only once in the topological mesh.
     * \note This is a costly operation.
     *
     * \tparam NonManifoldFaceCommand Command executed when non-manifold faces are
     * found. API and default implementation:
     * \snippet Core/Geometry/TopologicalMesh.cpp Default command implementation
     *
     */
    template <typename MeshIndex, typename NonManifoldFaceCommand>
    explicit TopologicalMesh( [[deprecated( "Use MultiIndexedGeometry instead" )]] const Ra::Core::
                                  Geometry::IndexedGeometry<MeshIndex>& mesh,
                              NonManifoldFaceCommand command );

    /**
     * Construct a topological mesh from a triangle mesh.
     * This operation merges vertex with same position, but keeps vertex
     * attributes on halfedges, so that TriangleMesh vertices with the same 3D
     * position are represented only once in the topological mesh.
     * \note This is a costly operation.
     *
     * \tparam NonManifoldFaceCommand Command executed when non-manifold faces are
     * found. API and default implementation:
     * \snippet Core/Geometry/TopologicalMesh.cpp Default command implementation
     * @todo, when MultiIndexedGeometry will be operational, will this replace the above ?
     *
     */
    template <typename NonManifoldFaceCommand>
    explicit TopologicalMesh(
        const Ra::Core::Geometry::MultiIndexedGeometry& mesh,
        const Ra::Core::Geometry::MultiIndexedGeometry::LayerKeyType& layerKey,
        NonManifoldFaceCommand command );

    inline void
    initWithWedge( const Ra::Core::Geometry::MultiIndexedGeometry& mesh,
                   const Ra::Core::Geometry::MultiIndexedGeometry::LayerKeyType& layerKey );

    template <typename NonManifoldFaceCommand>
    void initWithWedge( const Ra::Core::Geometry::MultiIndexedGeometry& mesh,
                        const Ra::Core::Geometry::MultiIndexedGeometry::LayerKeyType& layerKey,
                        NonManifoldFaceCommand command );
    /**
     * Return a triangleMesh from the topological mesh.
     * \note This is a costly operation.
     * \warning It uses the attributes defined on halfedges. Do not work well if attribs are defined
     * on wedges (initWithWedge), in this case use toTriangleMeshFromWedges()
     */
    TriangleMesh toTriangleMesh();

    /**
     * Return a triangleMesh from the topological mesh.
     * \note This is a costly operation.
     * \warning It uses the attributes defined on wedges
     */
    PolyMesh toPolyMesh();

    LineMesh toLineMesh();

    /**
     * Update triangle mesh data, assuming the mesh and this topo mesh has the
     * same topology.
     */
    void updateTriangleMesh( Ra::Core::Geometry::MultiIndexedGeometry& mesh );
    void updateTriangleMeshNormals( Ra::Core::Geometry::MultiIndexedGeometry& mesh );
    void updateTriangleMeshNormals( AttribArrayGeometry::NormalAttribHandle::Container& normals );

    void update( const Ra::Core::Geometry::MultiIndexedGeometry& mesh );
    void updateNormals( const Ra::Core::Geometry::MultiIndexedGeometry& mesh );
    void updatePositions( const Ra::Core::Geometry::MultiIndexedGeometry& mesh );
    void updatePositions( const AttribArrayGeometry::PointAttribHandle::Container& vertices );
    // import other version of halfedge_handle method
    using base::halfedge_handle;

    /**
     * Return the half-edge associated with a given vertex and face.
     * \note Asserts if vh is not a member of fh.
     */
    inline HalfedgeHandle halfedge_handle( VertexHandle vh, FaceHandle fh ) const;

    /// Import Base definition of normal and set normal.
    ///@{
    using base::normal;
    using base::set_normal;
    ///@}

    /**
     * Set the normal n to all the wedges that share to vh.
     * If you work with vertex normals, please call this function on all vertex
     * handles before convertion with toTriangleMesh.
     */
    inline void propagate_normal_to_wedges( VertexHandle vh );

    /**
     * Return a handle to the halfedge property storing vertices indices within
     * the TriangleMesh *this has been built on.
     */
    inline const OpenMesh::HPropHandleT<Index>& getInputTriangleMeshIndexPropHandle() const;

    /**
     * Return a handle to the halfedge property storing vertices indices within
     * the TriangleMesh returned by toTriangleMesh().
     * \note This property is valid only after toTriangleMesh() has been called.
     */
    inline const OpenMesh::HPropHandleT<Index>& getOutputTriangleMeshIndexPropHandle() const;

    /**
     * \name Dealing with normals
     * Utils to deal with normals when modifying the mesh topology.
     */
    ///@{

    void updateWedgeNormals();
    ///@}

    /// update wedge's position to correspond to current point position, which might not be the case
    /// if point(handle) = something; has been used.
    void copyPointsPositionToWedges();
    /**
     * \name Topological operations
     */
    ///@{
    /**
     * Apply a 2-4 edge split.
     * \param eh The handle to the edge to split.
     * \param f The interpolation factor to place the new point on the edge.
     *          Must be in [0,1].
     * \return True if the edge has been split, false otherwise.
     * \note Only applies on edges between 2 triangles, and if \a f is in [0,1].
     * \note Mesh attributes are linearly interpolated on the newly created
     * halfedge.
     * \note f=0 correspond to halfedge_handle( eh, 0 ) (i.e. first vertex of
     * the edge)
     */
    bool splitEdge( TopologicalMesh::EdgeHandle eh, Scalar f );

    /**
     * Halfedge collapes \a he.
     * vo=from_vertex_handle(he) is deleted.
     * After collapse vo incoming halfedges points to vh = to_vertex_handle(he).
     * Wedge indices are updated to reflect the change in topology.
     * For detailed topological modifications see \ref develmeshes.
     * \param he halfedge's hangle to collapse.
     */
    void collapse( HalfedgeHandle, bool = false );
    [[deprecated( "use collapse() instead." )]] void
    collapseWedge( TopologicalMesh::HalfedgeHandle he, bool keepFromVertex = false );
    ///@}

    /**
     * Return the set of WedgeIndex incident to a given Vertex \a vh.
     * only valid non deleted wedges are present in the set.
     */
    inline std::set<WedgeIndex> getVertexWedges( OpenMesh::VertexHandle vh ) const;

    /**
     * get the wedge index associated with an halfedge
     */
    inline WedgeIndex getWedgeIndex( OpenMesh::HalfedgeHandle heh ) const;

    /**
     * Access to wedge data.
     * \param idx must be valid and correspond to a non delete wedge index.
     */
    inline const WedgeData& getWedgeData( const WedgeIndex& idx ) const;
    template <typename T>
    [[deprecated( "use getWedgeAttrib() instead." )]] const T&
    getWedgeData( const WedgeIndex& idx, const std::string& name ) const;
    template <typename T>
    const T& getWedgeAttrib( const WedgeIndex& idx, const std::string& name ) const;

    /**
     * Return the wedge refcount, for debug purpose.
     */
    inline unsigned int getWedgeRefCount( const WedgeIndex& idx ) const;

    /** set WedgeData \a wd to the wedge with index \a widx.
     * All halfedge that point to widx will get the new values.
     * \param widx index of the wedge
     * \param wd data to set to wedge that correspond to widx
     */
    inline void setWedgeData( WedgeIndex widx, const WedgeData& wd );

    /**
     * Change the WedgeData associated for \a idx, for attrib \a name to \a value.
     * The data is changed for all halfedges referencing this wedge.
     * \return true if the wedge is set, false if nothing set, i.e. if name is not an attrib of
     * type T.
     */
    template <typename T>
    [[deprecated( "use setWedgeAttrib() instead." )]] inline bool
    setWedgeData( const WedgeIndex& idx, const std::string& name, const T& value );

    template <typename T>
    inline bool setWedgeAttrib( const WedgeIndex& idx, const std::string& name, const T& value );

    /** return a WedgeData with all attrib initialized to default values */
    inline WedgeData newWedgeData() const { return m_wedges.newWedgeData(); }
    /** return a WedgeData with position (and vertex handle) initialized from the value of he's to
     * vertex. */
    inline WedgeData newWedgeData( HalfedgeHandle he ) const {
        return m_wedges.newWedgeData( to_vertex_handle( he ), point( to_vertex_handle( he ) ) );
    }

    /**
     * Replace the wedge data associated with an halfedge.
     * The old wedge is "deleted". If wedge data correspond to an already
     * present wedge, it's index is used.
     */
    inline void replaceWedge( OpenMesh::HalfedgeHandle he, const WedgeData& wd );

    template <typename T>
    inline int addWedgeAttrib( const std::string& name, T value = {} ) {
        return m_wedges.addAttrib<T>( name, value );
    }

    /**
     * Replace the wedge index associated with an halfedge.
     * The old wedge is "deleted". The new wedge reference count is incremented.
     */
    inline void replaceWedgeIndex( OpenMesh::HalfedgeHandle he, const WedgeIndex& widx );

    /**
     * call mergeEquelWedges( vh ) for every vertices of the mesh.
     * \see void mergeEqualWedges( OpenMesh::VertexHandle vh );
     */
    inline void mergeEqualWedges();

    /**
     * Merge (make same index) wegdes with the same data around \a vh
     * \param vh vertex handle to process
     */
    inline void mergeEqualWedges( OpenMesh::VertexHandle vh );

    /// Remove deleted element from the mesh, including wedges.
    void garbage_collection();
    inline void clean() {
        base::clean();
        m_wedges.clean();
    }

    inline const std::vector<std::string>& getVec4AttribNames() const;
    inline const std::vector<std::string>& getVec3AttribNames() const;
    inline const std::vector<std::string>& getVec2AttribNames() const;
    inline const std::vector<std::string>& getFloatAttribNames() const;

    /// true if more than one wedge arount vertex \a vh, false if only one wedge
    inline bool isFeatureVertex( const VertexHandle& vh ) const;

    /// true if at least one of edge's vertex as two different wedge arount the
    /// edge.
    /// false if the two vertices have the same wedge for both face aside the edge.
    inline bool isFeatureEdge( const EdgeHandle& eh ) const;

    inline const OpenMesh::HPropHandleT<WedgeIndex>& getWedgeIndexPph() const;

    void delete_face( FaceHandle _fh, bool _delete_isolated_vertices = true );

    /**
     * is the vertex a "bow tie" vertex ?
     * \note Alias for OpenMesh::is_manifold
     */
    bool isManifold( VertexHandle vh ) const;

    /// Check if evrything looks right in the data structure
    /// \return true if ok, false if ko.
    bool checkIntegrity() const;

    void triangulate();

    /**
     * Inner class WedgeData represents the actual data per wedge, including position.
     *
     * At any time m_position as to be equal to the wedge's vertex point.
     * All wedges have the same set of attributes.
     * Access and management is delegated to TopologicalMesh and WedgeCollection
     */
    class WedgeData
    {
      public:
        EIGEN_MAKE_ALIGNED_OPERATOR_NEW

        explicit WedgeData() = default;
        inline bool operator==( const WedgeData& lhs ) const;
        inline bool operator!=( const WedgeData& lhs ) const;
        inline bool operator<( const WedgeData& lhs ) const;

        template <typename T>
        inline VectorArray<T>& getAttribArray();
        template <typename T>
        inline const VectorArray<T>& getAttribArray() const;
        friend Wedge;

        //        Index m_inputTriangleMeshIndex;
        //        Index m_outputTriangleMeshIndex;
        VertexHandle m_vertexHandle;
        Vector3 m_position {};
        VectorArray<Scalar> m_floatAttrib;
        VectorArray<Vector2> m_vector2Attrib;
        VectorArray<Vector3> m_vector3Attrib;
        VectorArray<Vector4> m_vector4Attrib;

      private:
        // return 1 : equals, 2: strict less, 3: strict greater
        template <typename T>
        static int compareVector( const T& a, const T& b );
    };

  private:
    // base on openmesh version
    void collapse_edge( HalfedgeHandle, bool );
    void collapse_loop( HalfedgeHandle );

    /**
     * This private class manage wedge data and refcount, to maintain deleted status
     *
     * \internal We need to export this class to make it accessible in .inl
     */
    class RA_CORE_API Wedge
    {
      public:
        EIGEN_MAKE_ALIGNED_OPERATOR_NEW

        explicit Wedge() {}
        explicit Wedge( const WedgeData& wd ) : m_wedgeData {wd}, m_refCount {1} {};
        const WedgeData& getWedgeData() const { return m_wedgeData; }
        void setWedgeData( const WedgeData& wedgeData ) { m_wedgeData = wedgeData; }
        void setWedgeData( WedgeData&& wedgeData ) { m_wedgeData = std::move( wedgeData ); }
        void incrementRefCount() { ++m_refCount; }
        void decrementRefCount() {
            if ( m_refCount ) --m_refCount;
        }
        /// comparison ignore refCount
        bool operator==( const Wedge& lhs ) const { return m_wedgeData == lhs.m_wedgeData; }

        bool isDeleted() const { return m_refCount == 0; }
        unsigned int getRefCount() const { return m_refCount; }

        friend WedgeCollection;
        friend TopologicalMesh;

      private:
        WedgeData& getWedgeData() { return m_wedgeData; }

        WedgeData m_wedgeData {};
        unsigned int m_refCount {0};
    };

    /**
     * This private class manage the wedge collection.
     * Most of the data members are public so that the enclosing class can
     * easily manage the data.
     *
     * \internal We need to export this class to make it accessible in .inl
     */
    class RA_CORE_API WedgeCollection
    {
      public:
        EIGEN_MAKE_ALIGNED_OPERATOR_NEW

        /**
         * Add wd to the wedge collection, and return the index.
         * If a wedge with same data is already present, it's index is returned,
         * otherwise a new wedge is added to the wedges collection.
         * \param wd Data to insert.
         * \return the index of the inserted (or found) wedge.
         */
        WedgeIndex add( const WedgeData& wd );

        /**
         * Delete the wedge \a idx from the collection.
         * These deletion actually just remove one reference from an halfedge
         * to the wedge data. If the wedge is still referenced by other
         * halfedges, it will not be removed during garbageCollection.
         */
        inline void del( const WedgeIndex& idx );
        WedgeIndex newReference( const WedgeIndex& idx );

        /**
         * Return the wedge data associated with \a idx
         */

        /// Return the wedge (not the data) for in class manipulation.
        /// client code should use getWedgeData only.
        inline const Wedge& getWedge( const WedgeIndex& idx ) const;

        inline const WedgeData& getWedgeData( const WedgeIndex& idx ) const;
        template <typename T>
        inline const T& getWedgeData( const WedgeIndex& idx, const std::string& name ) const;
        template <typename T>
        inline T& getWedgeData( const WedgeIndex& idx, int attribIndex );
        template <typename T>
        inline const T& getWedgeAttrib( const WedgeIndex& idx, const std::string& name ) const;
        template <typename T>
        inline T& getWedgeAttrib( const WedgeIndex& idx, int attribIndex );

        unsigned int getWedgeRefCount( const WedgeIndex& idx ) const;

        /// \see TopologicalMesh::setWedgeData
        inline void setWedgeData( const WedgeIndex& idx, const WedgeData& wd );

        /// change WedgeData member name to value.
        /// wd is moidified accordingly.
        /// \return false if name is not of type T
        /// \retrun true on sucess
        template <typename T>
        inline bool
        setWedgeAttrib( TopologicalMesh::WedgeData& wd, const std::string& name, const T& value );
        template <typename T>
        inline bool
        setWedgeAttrib( const WedgeIndex& idx, const std::string& name, const T& value );
        template <typename T>
        inline void setWedgeAttrib( const TopologicalMesh::WedgeIndex& idx,
                                    const int& attribIndex,
                                    const T& value );

        template <typename T>
        inline int getWedgeAttribIndex( const std::string& name );

        inline bool setWedgePosition( const WedgeIndex& idx, const Vector3& value );

        /// management

        template <typename T>
        inline const std::vector<std::string>& getNameArray() const;

        // name is supposed to be unique within all attribs
        // not checks are performed
        // return the index of the the newly added attrib.
        template <typename T>
        int addAttribName( const std::string& name );

        // add attrib to all wedges with default value value
        template <typename T>
        int addAttrib( const std::string& name, const T& value = {} );

        /// return the offset ot apply to each wedgeindex so that
        /// after garbageCollection all indices are valid and coherent.
        std::vector<int> computeCleanupOffset() const;
        /// \todo removeDuplicateWedge
        /// merge wedges with same data
        /// return old->new index correspondance to update wedgeIndexPph
        /// inline void removeDuplicateWedge

        inline size_t size() const { return m_data.size(); }

        /// remove unreferenced wedge, halfedges need to be reindexed.
        inline void garbageCollection();

        inline void clean();

        // return a new wedgeData with uninit values.
        inline WedgeData newWedgeData() const;
        inline WedgeData newWedgeData( TopologicalMesh::VertexHandle vh,
                                       TopologicalMesh::Point p ) const;

        ///\ todo       private:
        /// attrib names associated to vertex/wedges, getted from CoreMesh, if any,
        std::vector<std::string> m_floatAttribNames;
        std::vector<std::string> m_vector2AttribNames;
        std::vector<std::string> m_vector3AttribNames;
        std::vector<std::string> m_vector4AttribNames;

        /// attrib handle from the CoreMesh given at construction, if any.
        /// used by TopologicalMesh::update()
        std::vector<AttribHandle<Scalar>> m_wedgeFloatAttribHandles;
        std::vector<AttribHandle<Vector2>> m_wedgeVector2AttribHandles;
        std::vector<AttribHandle<Vector3>> m_wedgeVector3AttribHandles;
        std::vector<AttribHandle<Vector4>> m_wedgeVector4AttribHandles;

        template <typename T>
        inline std::vector<std::string>& getNameArray();
        AlignedStdVector<Wedge> m_data;
    };

    // internal function to build Core Mesh attribs correspondance to wedge attribs.
    class InitWedgeAttribsFromMultiIndexedGeometry
    {
      public:
        InitWedgeAttribsFromMultiIndexedGeometry(
            TopologicalMesh* topo,
            const Ra::Core::Geometry::MultiIndexedGeometry& triMesh ) :
            m_topo( topo ), m_triMesh( triMesh ) {}
        void operator()( AttribBase* attr ) const;

      private:
        TopologicalMesh* m_topo;
        const Ra::Core::Geometry::MultiIndexedGeometry& m_triMesh;
    };

    //! [Default command implementation]
    /// \todo add layerKey
    struct DefaultNonManifoldFaceCommand {
        /// \brief details string is printed along with the message
        DefaultNonManifoldFaceCommand( const std::string& details = {} ) : m_details {details} {}
        /// \brief Initalize with input Ra::Core::Geometry::TriangleMesh
        inline void initialize( const Ra::Core::Geometry::MultiIndexedGeometry& ) {}
        /// \brief Process non-manifold face
        inline void process( const std::vector<TopologicalMesh::VertexHandle>& /*face_vhandles*/ ) {
            LOG( logWARNING ) << "Invalid face handle returned : face not added " + m_details;
        }
        /// \brief If needed, apply post-processing on the Ra::Core::Geometry::TopologicalMesh
        inline void postProcess( TopologicalMesh& ) {}
        //! [Default command implementation]
      private:
        std::string m_details;
    };

    WedgeData interpolateWedgeAttributes( const WedgeData&, const WedgeData&, Scalar alpha );

    /// @todo when MultiIndexView will be operational, remove the IndexedGeometry<U> version above
    template <typename T>
    inline void copyAttribToWedgeData( const Ra::Core::Geometry::MultiIndexedGeometry& mesh,
                                       unsigned int vindex,
                                       const std::vector<AttribHandle<T>>& attrHandleVec,
                                       VectorArray<T>* to );

    /// @todo when MultiIndexView will be operational, remove the IndexedGeometry<T> version above
    inline void copyMeshToWedgeData( const Ra::Core::Geometry::MultiIndexedGeometry& mesh,
                                     unsigned int vindex,
                                     const std::vector<AttribHandle<Scalar>>& wprop_float,
                                     const std::vector<AttribHandle<Vector2>>& wprop_vec2,
                                     const std::vector<AttribHandle<Vector3>>& wprop_vec3,
                                     const std::vector<AttribHandle<Vector4>>& wprop_vec4,
                                     TopologicalMesh::WedgeData* wd );

    template <typename T>
    using HandleAndValueVector =
        std::vector<std::pair<AttribHandle<T>, T>,
                    Eigen::aligned_allocator<std::pair<AttribHandle<T>, T>>>;

    void split_copy( EdgeHandle _eh, VertexHandle _vh );
    void split( EdgeHandle _eh, VertexHandle _vh );

    OpenMesh::HPropHandleT<WedgeIndex> m_wedgeIndexPph; /**< Halfedges' Wedge index */
    WedgeCollection m_wedges;                           /**< Wedge data management */

    ///\todo to be deleted/updated
    OpenMesh::HPropHandleT<Index> m_inputTriangleMeshIndexPph;
    OpenMesh::HPropHandleT<Index> m_outputTriangleMeshIndexPph;

    int m_normalsIndex {-1};
    // vertex handle idx -> face handle idx -> wedge idx with the same normal
    std::vector<std::map<int, std::vector<int>>> m_vertexFaceWedgesWithSameNormals;

    friend class TMOperations;
};

// heplers
void printWedgesInfo( const TopologicalMesh& );

} // namespace Geometry
} // namespace Core
} // namespace Ra

#include <Core/Geometry/TopologicalMesh.inl>


/* Boost Graph example, from Boost-Cookbook + minor adaptations for testcase
   source: https://github.com/apolukhin/Boost-Cookbook/blob/second_edition/Chapter12/01_graph/main.cpp

  Boost Software License - Version 1.0 - August 17th, 2003

  Permission is hereby granted, free of charge, to any person or organization
  obtaining a copy of the software and accompanying documentation covered by
  this license (the "Software") to use, reproduce, display, distribute,
  execute, and transmit the Software, and to prepare derivative works of the
  Software, and to permit third-parties to whom the Software is furnished to
  do so, all subject to the following:

  The copyright notices in the Software and this entire statement, including
  the above license grant, this restriction and the following disclaimer,
  must be included in all copies of the Software, in whole or in part, and
  all derivative works of the Software, unless such copies or derivative
  works are solely in the form of machine-executable object code generated by
  a source language processor.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
  SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
  FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  DEALINGS IN THE SOFTWARE.
*/

#include <boost/graph/adjacency_list.hpp>
#include <string>

typedef std::string vertex_t;

typedef boost::adjacency_list <
boost::vecS,
boost::vecS,
boost::bidirectionalS,
vertex_t> graph_type;

#include <boost/utility/string_ref.hpp>
#include <iostream>

inline bool find_and_print( const graph_type& graph, boost::string_ref name ) {
  typedef boost::graph_traits<graph_type>::vertex_iterator vert_it_t;

  vert_it_t it, end;
  boost::tie(it, end) = boost::vertices(graph);

  typedef boost::graph_traits<graph_type>::vertex_descriptor desc_t;

  for ( ; it != end; ++it ) {
    const desc_t desc = *it;
    const vertex_t& vertex = boost::get(boost::vertex_bundle, graph)[desc];

    if ( vertex == name.data()) {
      break;
    }
  }

  if ( it != end ) {
    std::cout << name << '\n';
    return true;
  }
  else {
    return false;
  }
}

int main() {
  graph_type graph;

  static const std::size_t vertex_count = 5;
  graph.m_vertices.reserve(vertex_count);

  typedef boost::graph_traits<graph_type>::vertex_descriptor descriptor_t;

  descriptor_t cpp = boost::add_vertex(vertex_t("C++"), graph);
  descriptor_t stl = boost::add_vertex(vertex_t("STL"), graph);
  descriptor_t boost = boost::add_vertex(vertex_t("Boost"), graph);
  descriptor_t guru = boost::add_vertex(vertex_t("C++ guru"), graph);
  descriptor_t ansic = boost::add_vertex(vertex_t("C"), graph);

  BOOST_STATIC_ASSERT(( boost::is_same<descriptor_t, std::size_t>::value ));

  boost::add_edge(cpp, stl, graph);
  boost::add_edge(stl, boost, graph);
  boost::add_edge(boost, guru, graph);
  boost::add_edge(ansic, guru, graph);

  if ( find_and_print(graph, "Boost") ) {
    return 0;
  }
  else {
    return 1;
  }
}


/*=============================================================================
  Copyright (c) 2010-2016 Bolero MURAKAMI
  https://github.com/bolero-MURAKAMI/Sprig

  Distributed under the Boost Software License, Version 1.0. (See accompanying
  file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
=============================================================================*/
#ifndef SPRIG_NTL_RATIONAL_HPP
#define SPRIG_NTL_RATIONAL_HPP

#include <sprig/config/config.hpp>

#ifdef SPRIG_USING_PRAGMA_ONCE
#	pragma once
#endif	// #ifdef SPRIG_USING_PRAGMA_ONCE

#include <boost/rational.hpp>
#include <boost/type_traits/is_integral.hpp>
#include <boost/type_traits/is_float.hpp>
#include <boost/utility/enable_if.hpp>
#include <sprig/external/ntl/zz.hpp>
#include <sprig/external/ntl/rr.hpp>
#include <sprig/type_traits/is_call_copy_param.hpp>
#include <sprig/type_traits/is_c_str.hpp>
#include <sprig/type_traits/is_basic_string.hpp>
#include <sprig/ntl/to_RR.hpp>
#include <sprig/ntl/significant.hpp>
#include <sprig/ntl/big_int.hpp>

namespace sprig {
	//
	// typedefs
	//
	typedef boost::rational<big_int> big_rational;

	//
	// to_rational
	//
	template<typename T>
	SPRIG_INLINE typename boost::enable_if<sprig::is_call_copy_param<T>, big_rational>::type
	to_rational(T const target) {
		NTL::RR value = to_RR(target);
		big_rational result(value.mantissa(), 1);
		if (value.exponent() >= 0) {
			result *= NTL::power(to_ZZ(2), value.exponent());
		} else {
			result /= NTL::power(to_ZZ(2), -value.exponent());
		}
		return result;
	}
	template<typename T>
	SPRIG_INLINE typename boost::disable_if<sprig::is_call_copy_param<T>, big_rational>::type
	to_rational(T const& target) {
		NTL::RR value = to_RR(target);
		big_rational result(value.mantissa(), 1);
		if (value.exponent() >= 0) {
			result *= NTL::power(to_ZZ(2), value.exponent());
		} else {
			result /= NTL::power(to_ZZ(2), -value.exponent());
		}
		return result;
	}

	//
	// to_rational_significant
	//
	// specialization to_rational_significant: is_integral(T)
	//
	template<typename T>
	SPRIG_INLINE typename boost::enable_if<
		boost::is_integral<T>,
		big_rational
	>::type
	to_rational_significant(T const target) {
		return big_rational(to_ZZ_full_significant(target));
	}
	//
	// specialization to_rational_significant: is_float(T)
	//
	template<typename T>
	SPRIG_INLINE typename boost::enable_if<
		boost::is_float<T>,
		big_rational
	>::type
	to_rational_significant(T const target) {
		return big_rational(to_ZZ_full_significant(target), significant<T>::digits10_ZZ());
	}
	//
	// specialization to_rational_significant: is_c_str(T)
	//
	template<typename T>
	SPRIG_INLINE typename boost::enable_if<
		is_c_str<T>,
		big_rational
	>::type
	to_rational_significant(T const target) {
		return big_rational(
			to_ZZ_full_significant(target),
			NTL::power(to_ZZ(10), static_cast<long>(significant<T>::digits(target)))
			);
	}
	//
	// specialization to_rational_significant: is_basic_string(T)
	//
	template<typename T>
	SPRIG_INLINE typename boost::enable_if<
		is_basic_string<T>,
		big_rational
	>::type
	to_rational_significant(T const& target) {
		return big_rational(
			to_ZZ_full_significant(target),
			NTL::power(to_ZZ(10), static_cast<long>(significant<T>::digits(target)))
			);
	}
}	// namespace sprig

#endif	// #ifndef SPRIG_NTL_RATIONAL_HPP


/*
 * MIT License
 *
 * Copyright (c) 2019 Camille Schreck
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 *
 *----------------------------------------------------------------------------
 * Circle Obstalce SOP
 *---------------------------------------------------------------------------
 * Create set of point sources along an offset surface of the obstacle.
 * Range of wavelength (and time step per wl), and is copied from input geometry, as well as
 *    the detail attibute.
 * Create on primitve and subset of sources for each wl.
 * Spacing depends on the wavelength and the parameter "density".
 * I use this node also to create a set of point sampling the border of the obstacle (offset=0)
 *    for the boundary conditions.
 * Note: the density of the boundary points should be at least twice the density of the
 *    sources.
 * Note 2: for the aperiodic version, do not forget to check the "interactive sources" box in
 *   the parameter of the node creating the sources of the obstacle.
 */

#ifndef __SOP_Circle_Obstacle_Src_h__
#define __SOP_Circle_Obstacle_Src_h__

#include <SOP/SOP_Node.h>
#include "definitions.hpp"
#include <Eigen/SVD>

class SOP_Circle_Obstacle_Src : public SOP_Node {

public:
  SOP_Circle_Obstacle_Src(OP_Network *net, const char *name, OP_Operator *op);
  virtual ~SOP_Circle_Obstacle_Src();

  static PRM_Template myTemplateList[];
  static OP_Node *myConstructor(OP_Network*, const char *, OP_Operator *);

  virtual OP_ERROR             cookInputGroups(OP_Context &context,
					       int alone = 0);

protected:
  virtual OP_ERROR cookMySop(OP_Context &context);
private:
  void        getGroups(UT_String &str){ evalString(str, "group", 0, 0); }
  fpreal      OFF(fpreal t)            { return evalFloat("off", 0, t); }
  fpreal      DENSITY(fpreal t)          { return evalFloat("density", 0, t); }
  fpreal      RADIUS(fpreal t)          { return evalFloat("radius", 0, t); }
  fpreal      CX(fpreal t) { return evalFloat("center", 0, t); }
  fpreal      CY(fpreal t) { return evalFloat("center", 1, t); }
  fpreal      CZ(fpreal t) { return evalFloat("center", 2, t); }
  int        INTER_SRC(fpreal t) {return evalInt("inter_src", 0, t);}

  const GA_PointGroup *myGroup;

};

#endif


/*!
  \file gpp_python_model_selection.cpp
  \rst
  This file has the logic to invoke C++ functions pertaining to model selection from Python.
  The data flow follows the basic 4 step from gpp_python_common.hpp.

  Note: several internal functions of this source file are only called from Export*() functions,
  so their description, inputs, outputs, etc. comments have been moved. These comments exist in
  Export*() as Python docstrings, so we saw no need to repeat ourselves.
\endrst*/
// This include violates the Google Style Guide by placing an "other" system header ahead of C and C++ system headers.  However,
// it needs to be at the top, otherwise compilation fails on some systems with some versions of python: OS X, python 2.7.3.
// Putting this include first prevents pyport from doing something illegal in C++; reference: http://bugs.python.org/issue10910
#include "Python.h"  // NOLINT(build/include)

#include "gpp_python_model_selection.hpp"

// NOLINT-ing the C, C++ header includes as well; otherwise cpplint gets confused
#include <algorithm>  // NOLINT(build/include_order)
#include <limits>  // NOLINT(build/include_order)
#include <string>  // NOLINT(build/include_order)
#include <vector>  // NOLINT(build/include_order)

#include <boost/python/def.hpp>  // NOLINT(build/include_order)
#include <boost/python/dict.hpp>  // NOLINT(build/include_order)
#include <boost/python/extract.hpp>  // NOLINT(build/include_order)
#include <boost/python/list.hpp>  // NOLINT(build/include_order)
#include <boost/python/object.hpp>  // NOLINT(build/include_order)

#include "gpp_common.hpp"
#include "gpp_covariance.hpp"
#include "gpp_domain.hpp"
#include "gpp_exception.hpp"
#include "gpp_geometry.hpp"
#include "gpp_model_selection.hpp"
#include "gpp_optimizer_parameters.hpp"
#include "gpp_python_common.hpp"

namespace optimal_learning {

namespace {

double ComputeLogLikelihoodWrapper(const boost::python::list& points_sampled,
                                   const boost::python::list& points_sampled_value,
                                   int dim, int num_sampled,
                                   LogLikelihoodTypes objective_type,
                                   const boost::python::list& hyperparameters,
                                   const boost::python::list& derivatives,
                                   int num_derivatives,
                                   const boost::python::list& noise_variance) {
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;

  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  switch (objective_type) {
    case LogLikelihoodTypes::kLogMarginalLikelihood: {
      LogMarginalLikelihoodEvaluator log_marginal_eval(input_container.points_sampled.data(),
                                                       input_container.points_sampled_value.data(),
                                                       input_container.derivatives.data(), input_container.num_derivatives,
                                                       input_container.dim, input_container.num_sampled);
      LogMarginalLikelihoodState log_marginal_state(log_marginal_eval, sqexp, input_container.noise_variance);

      double log_likelihood = log_marginal_eval.ComputeLogLikelihood(log_marginal_state);
      return log_likelihood;
    }  // end case LogLikelihoodTypes::kLogMarginalLikelihood
/*    case LogLikelihoodTypes::kLeaveOneOutLogLikelihood: {
      LeaveOneOutLogLikelihoodEvaluator leave_one_out_eval(input_container.points_sampled.data(),
                                                           input_container.points_sampled_value.data(),
                                                           input_container.noise_variance.data(),
                                                           input_container.dim,
                                                           input_container.num_sampled);
      LeaveOneOutLogLikelihoodState leave_one_out_state(leave_one_out_eval, matern_25);

      double loo_likelihood = leave_one_out_eval.ComputeLogLikelihood(leave_one_out_state);
      return loo_likelihood;
    }*/
    default: {
      double log_likelihood = -std::numeric_limits<double>::max();
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid objective mode choice. Setting log likelihood to -DBL_MAX.");
      return log_likelihood;
    }
  }  // end switch over objective_type
}

boost::python::list ComputeHyperparameterGradLogLikelihoodWrapper(const boost::python::list& points_sampled,
                                                                  const boost::python::list& points_sampled_value,
                                                                  int dim, int num_sampled,
                                                                  LogLikelihoodTypes objective_type,
                                                                  const boost::python::list& hyperparameters,
                                                                  const boost::python::list& derivatives,
                                                                  int num_derivatives,
                                                                  const boost::python::list& noise_variance) {
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;

  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  std::vector<double> grad_log_likelihood(sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives);
  switch (objective_type) {
    case LogLikelihoodTypes::kLogMarginalLikelihood: {
      LogMarginalLikelihoodEvaluator log_marginal_eval(input_container.points_sampled.data(),
                                                       input_container.points_sampled_value.data(),
                                                       input_container.derivatives.data(), input_container.num_derivatives,
                                                       input_container.dim, input_container.num_sampled);
      LogMarginalLikelihoodState log_marginal_state(log_marginal_eval, sqexp, input_container.noise_variance);

      log_marginal_eval.ComputeGradLogLikelihood(&log_marginal_state, grad_log_likelihood.data());
      break;
    }  // end case LogLikelihoodTypes::kLogMarginalLikelihood
/*    case LogLikelihoodTypes::kLeaveOneOutLogLikelihood: {
      LeaveOneOutLogLikelihoodEvaluator leave_one_out_eval(input_container.points_sampled.data(),
                                                           input_container.points_sampled_value.data(),
                                                           input_container.noise_variance.data(),
                                                           input_container.dim,
                                                           input_container.num_sampled);
      LeaveOneOutLogLikelihoodState leave_one_out_state(leave_one_out_eval, matern_25);

      leave_one_out_eval.ComputeGradLogLikelihood(&leave_one_out_state, grad_log_likelihood.data());
      break;
    }*/
    default: {
      std::fill(grad_log_likelihood.begin(), grad_log_likelihood.end(), std::numeric_limits<double>::max());
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid objective mode choice. Setting all gradients to DBL_MAX.");
      break;
    }
  }  // end switch over objective_type
  return VectorToPylist(grad_log_likelihood);
}

/*!\rst
  Utility that dispatches log likelihood optimization (wrt hyperparameters) based on optimizer type.
  This is just used to reduce copy-pasted code.

  Let n_hyper = covariance.GetNumberOfHyperparameters();

  \param
    :optimizer_parameters: python/cpp_wrappers/optimization._CppOptimizerParameters
      Python object containing the LogLikelihoodTypes objective_type and OptimizerTypes optimzer_typ
      to use as well as appropriate parameter structs e.g., NewtonParameters for type kNewton).
      See comments on the python interface for multistart_hyperparameter_optimization_wrapper
    :log_likelihood_eval: object supporting evaluation of log likelihood
    :covariance: the CovarianceFunction object encoding assumptions about the GP's behavior on our data
    :hyperparameter_domain[2][n_hyper]: matrix specifying the boundaries of a n_hyper-dimensional tensor-product
      domain.  Specified as a list of [x_i_min, x_i_max] pairs, i = 0 .. dim-1
      Specify in LOG-10 SPACE!
    :optimizer_type: type of optimization to use (e.g., null, gradient descent)
    :max_num_threads: maximum number of threads for use by OpenMP (generally should be <= # cores)
    :randomness_source: object containing randomness sources for generating random points in the domain
    :status: pydict object; cannot be None
  \output
    :randomness_source: PRNG internal states modified
    :status: modified on exit to describe whether convergence occurred
    :new_hyperparameters[n_hyper]: new hyperparameters found by optimizer to maximize the specified log likelihood measure
\endrst*/
template <typename LogLikelihoodEvaluator>
void DispatchHyperparameterOptimization(const boost::python::object& optimizer_parameters,
                                        const LogLikelihoodEvaluator& log_likelihood_eval,
                                        const CovarianceInterface& covariance,
                                        const std::vector<double> noise_variance,
                                        ClosedInterval const * restrict hyperparameter_domain,
                                        OptimizerTypes optimizer_type, int max_num_threads,
                                        RandomnessSourceContainer& randomness_source,
                                        boost::python::dict& status,
                                        double * restrict new_hyperparameters) {
  bool found_flag = false;
  switch (optimizer_type) {
    case OptimizerTypes::kNull: {
      // optimizer_parameters must contain an int num_random_samples field, extract it
      int num_random_samples = boost::python::extract<int>(optimizer_parameters.attr("num_random_samples"));
      ThreadSchedule thread_schedule(max_num_threads, omp_sched_guided);
      LatinHypercubeSearchHyperparameterOptimization(log_likelihood_eval, covariance, noise_variance, hyperparameter_domain,
                                                     thread_schedule, num_random_samples, &found_flag,
                                                     &randomness_source.uniform_generator, new_hyperparameters);
      status[std::string(log_likelihood_eval.kName) + "_lhc_found_update"] = found_flag;
      break;
    }  // end case kNull for optimizer_type
    case OptimizerTypes::kGradientDescent: {
      // optimizer_parameters must contain a optimizer_parameters field
      // of type GradientDescentParameters. extract it
      const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));
      ThreadSchedule thread_schedule(max_num_threads, omp_sched_dynamic);
      MultistartGradientDescentHyperparameterOptimization(log_likelihood_eval, covariance, noise_variance,
                                                          gradient_descent_parameters,
                                                          hyperparameter_domain,
                                                          thread_schedule, &found_flag,
                                                          &randomness_source.uniform_generator,
                                                          new_hyperparameters);
      status[std::string(log_likelihood_eval.kName) + "_gradient_descent_found_update"] = found_flag;
      break;
    }  // end case kGradientDescent for optimizer_type
/*    case OptimizerTypes::kNewton: {
      // optimizer_parameters must contain a optimizer_parameters field
      // of type NewtonParameters. extract it
      const NewtonParameters& newton_parameters = boost::python::extract<NewtonParameters&>(optimizer_parameters.attr("optimizer_parameters"));
      ThreadSchedule thread_schedule(max_num_threads, omp_sched_dynamic);
      MultistartNewtonHyperparameterOptimization(log_likelihood_eval, covariance,
                                                 newton_parameters, hyperparameter_domain,
                                                 thread_schedule, &found_flag,
                                                 &randomness_source.uniform_generator,
                                                 new_hyperparameters);
      status[std::string(log_likelihood_eval.kName) + "_newton_found_update"] = found_flag;
      break;
    }  // end case kNewton for optimizer_type */
    default: {
      std::fill(new_hyperparameters, new_hyperparameters + covariance.GetNumberOfHyperparameters() + noise_variance.size(), 1.0);
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid optimizer choice. Setting all hyperparameters to 1.0.");
      break;
    }
  }  // end switch over optimzer_type for LogLikelihoodTypes::kLogMarginalLikelihood
}

boost::python::list MultistartHyperparameterOptimizationWrapper(const boost::python::object& optimizer_parameters,
                                                                const boost::python::list& hyperparameter_domain,
                                                                const boost::python::list& points_sampled,
                                                                const boost::python::list& points_sampled_value,
                                                                int dim, int num_sampled,
                                                                const boost::python::list& hyperparameters,
                                                                const boost::python::list& noise_variance,
                                                                const boost::python::list& derivatives,
                                                                int num_derivatives, int max_num_threads,
                                                                RandomnessSourceContainer& randomness_source,
                                                                boost::python::dict& status) {
  // TODO(GH-131): make domain objects constructible from python; and pass them in through
  // the optimizer_parameters python object
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;
  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  int num_hyperparameters = sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives;
  std::vector<double> new_hyperparameters(num_hyperparameters);

  std::vector<ClosedInterval> hyperparameter_domain_C(num_hyperparameters);
  CopyPylistToClosedIntervalVector(hyperparameter_domain, num_hyperparameters, hyperparameter_domain_C);

  OptimizerTypes optimizer_type = boost::python::extract<OptimizerTypes>(optimizer_parameters.attr("optimizer_type"));
  LogLikelihoodTypes objective_type = boost::python::extract<LogLikelihoodTypes>(optimizer_parameters.attr("objective_type"));
  switch (objective_type) {
    case LogLikelihoodTypes::kLogMarginalLikelihood: {
      LogMarginalLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                         input_container.points_sampled_value.data(),
                                                         input_container.derivatives.data(), input_container.num_derivatives,
                                                         input_container.dim, input_container.num_sampled);

      DispatchHyperparameterOptimization(optimizer_parameters, log_likelihood_eval, sqexp, input_container.noise_variance,
                                         hyperparameter_domain_C.data(), optimizer_type, max_num_threads,
                                         randomness_source, status, new_hyperparameters.data());
      break;
    }  // end case LogLikelihoodTypes::kLogMarginalLikelihood
/*    case LogLikelihoodTypes::kLeaveOneOutLogLikelihood: {
      LeaveOneOutLogLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                            input_container.points_sampled_value.data(),
                                                            input_container.noise_variance.data(),
                                                            input_container.dim, input_container.num_sampled);

      DispatchHyperparameterOptimization(optimizer_parameters, log_likelihood_eval, matern_25,
                                         hyperparameter_domain_C.data(), optimizer_type, max_num_threads,
                                         randomness_source, status, new_hyperparameters.data());
      break;
    }  // end case LogLikelihoodTypes::kLeaveOneOutLogLikelihood*/
    default: {
      std::fill(new_hyperparameters.begin(), new_hyperparameters.end(), 1.0);
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid objective type choice. Setting all hyperparameters to 1.0.");
      break;
    }
  }  // end switch over objective_type

  return VectorToPylist(new_hyperparameters);
}

boost::python::list EvaluateLogLikelihoodAtHyperparameterListWrapper(const boost::python::list& hyperparameter_list,
                                                                     const boost::python::list& points_sampled,
                                                                     const boost::python::list& points_sampled_value,
                                                                     int dim, int num_sampled,
                                                                     LogLikelihoodTypes objective_mode,
                                                                     const boost::python::list& hyperparameters,
                                                                     const boost::python::list& noise_variance,
                                                                     const boost::python::list& derivatives,
                                                                     int num_derivatives,
                                                                     int num_multistarts, int max_num_threads,
                                                                     boost::python::dict& status) {
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;
  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  std::vector<double> new_hyperparameters_C(sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives);
  std::vector<double> result_function_values_C(num_multistarts);
  std::vector<double> initial_guesses_C((sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives) * num_multistarts);

  CopyPylistToVector(hyperparameter_list, (sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives) * num_multistarts, initial_guesses_C);

  TensorProductDomain dummy_domain(nullptr, 0);
  ThreadSchedule thread_schedule(max_num_threads, omp_sched_guided);

  bool found_flag = false;
  switch (objective_mode) {
    case LogLikelihoodTypes::kLogMarginalLikelihood: {
      LogMarginalLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                         input_container.points_sampled_value.data(),
                                                         input_container.derivatives.data(), input_container.num_derivatives,
                                                         input_container.dim, input_container.num_sampled);
      EvaluateLogLikelihoodAtPointList(log_likelihood_eval, sqexp, input_container.noise_variance, dummy_domain, thread_schedule,
                                       initial_guesses_C.data(), num_multistarts, &found_flag,
                                       result_function_values_C.data(), new_hyperparameters_C.data());
      status[std::string("evaluate_") + log_likelihood_eval.kName + "_at_hyperparameter_list"] = found_flag;
      break;
    }
/*    case LogLikelihoodTypes::kLeaveOneOutLogLikelihood: {
      LeaveOneOutLogLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                            input_container.points_sampled_value.data(),
                                                            input_container.noise_variance.data(),
                                                            input_container.dim, input_container.num_sampled);
      EvaluateLogLikelihoodAtPointList(log_likelihood_eval, matern_25, dummy_domain, thread_schedule,
                                       initial_guesses_C.data(), num_multistarts, &found_flag,
                                       result_function_values_C.data(), new_hyperparameters_C.data());
      status[std::string("evaluate_") + log_likelihood_eval.kName + "_at_hyperparameter_list"] = found_flag;
      break;
    }*/
    default: {
      std::fill(result_function_values_C.begin(), result_function_values_C.end(), -std::numeric_limits<double>::max());
      status["evaluate_invalid_log_likelihood_at_hyperparameter_list"] = found_flag;
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid objective mode choice. Setting all results to -DBL_MAX.");
      break;
    }
  }

  return VectorToPylist(result_function_values_C);
}

boost::python::list RestartedGradientDescentHyperparameterOptimizationWrapper(const boost::python::object& optimizer_parameters,
                                                                              const boost::python::list& hyperparameter_domain,
                                                                              const boost::python::list& points_sampled,
                                                                              const boost::python::list& points_sampled_value,
                                                                              int dim, int num_sampled,
                                                                              const boost::python::list& hyperparameters,
                                                                              const boost::python::list& noise_variance,
                                                                              const boost::python::list& derivatives,
                                                                              int num_derivatives,
                                                                              boost::python::dict& status){
  // the optimizer_parameters python object
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;
  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  int num_hyperparameters = sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives;
  std::vector<double> new_hyperparameters(num_hyperparameters);

  std::vector<ClosedInterval> hyperparameter_domain_C(num_hyperparameters);
  CopyPylistToClosedIntervalVector(hyperparameter_domain, num_hyperparameters, hyperparameter_domain_C);

  LogMarginalLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                     input_container.points_sampled_value.data(),
                                                     input_container.derivatives.data(), input_container.num_derivatives,
                                                     input_container.dim, input_container.num_sampled);
  const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));
  RestartedGradientDescentHyperparameterOptimizationTensor(log_likelihood_eval, sqexp, input_container.noise_variance, gradient_descent_parameters,
                                                           hyperparameter_domain_C.data(), new_hyperparameters.data());
  return VectorToPylist(new_hyperparameters);
}

}  // end unnamed namespace

void ExportModelSelectionFunctions() {
  boost::python::def("compute_log_likelihood", ComputeLogLikelihoodWrapper, R"%%(
    Computes the specified log likelihood measure of model fit using the given
    hyperparameters.

    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param objective_mode: describes which log likelihood measure to compute (e.g., kLogMarginalLikelihood)
    :type objective_mode: GPP.LogLikelihoodTypes (enum)
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :return: computed log marginal likelihood of prior
    :rtype: float64
    )%%");

  boost::python::def("compute_hyperparameter_grad_log_likelihood", ComputeHyperparameterGradLogLikelihoodWrapper, R"%%(
    Computes the gradient of the specified log likelihood measure of model fit using the given
    hyperparameters. Gradient computed wrt the given hyperparameters.

    ``n_hyper`` denotes the number of hyperparameters.

    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param objective_mode: describes which log likelihood measure to compute (e.g., kLogMarginalLikelihood)
    :type objective_mode: GPP.LogLikelihoodTypes (enum)
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :return: gradients of log marginal likelihood wrt hyperparameters
    :rtype: list of float64 with shape (num_hyperparameters, )
    )%%");

  boost::python::def("multistart_hyperparameter_optimization", MultistartHyperparameterOptimizationWrapper, R"%%(
    Optimize the specified log likelihood measure over the specified domain using the specified optimization method.

    The _CppOptimizerParameters object is a python class defined in:
    ``python/cpp_wrappers/optimization._CppOptimizerParameters``
    See that class definition for more details.

    This function expects it to have the fields:

    * objective_type (LogLikelihoodTypes enum from this file)
    * optimizer_type (OptimizerTypes enum from this file)
    * num_random_samples (int, number of samples to 'dumb' search over, only used if optimizer_type == kNull)
    * optimizer_parameters (*Parameters struct (gpp_optimizer_parameters.hpp) where * matches optimizer_type
      unused if optimizer_type == kNull)

    ``n_hyper`` denotes the number of hyperparameters.

    :param optimizer_parameters: python object containing the LogLikelihoodTypes
      objective to use, OptimizerTypes optimzer_type to use as well as appropriate
      parameter structs e.g., NewtonParameters for type kNewton
    :type optimizer_parameters:  _CppOptimizerParameters
    :param hyperparameter_domain: [lower, upper] bound pairs for each hyperparameter dimension in LOG-10 SPACE
    :type hyperparameter_domain: list of float64 with shape (num_hyperparameters, 2)
    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: optimized hyperparameters
    :rtype: list of float64 with shape (num_hyperparameters, )
    )%%");

  boost::python::def("restarted_hyperparameter_optimization", RestartedGradientDescentHyperparameterOptimizationWrapper, R"%%(
    Optimize the specified log likelihood measure over the specified domain using the specified optimization method.

    The _CppOptimizerParameters object is a python class defined in:
    ``python/cpp_wrappers/optimization._CppOptimizerParameters``
    See that class definition for more details.

    This function expects it to have the fields:

    * objective_type (LogLikelihoodTypes enum from this file)
    * optimizer_type (OptimizerTypes enum from this file)
    * num_random_samples (int, number of samples to 'dumb' search over, only used if optimizer_type == kNull)
    * optimizer_parameters (*Parameters struct (gpp_optimizer_parameters.hpp) where * matches optimizer_type
      unused if optimizer_type == kNull)

    ``n_hyper`` denotes the number of hyperparameters.

    :param optimizer_parameters: python object containing the LogLikelihoodTypes
      objective to use, OptimizerTypes optimzer_type to use as well as appropriate
      parameter structs e.g., NewtonParameters for type kNewton
    :type optimizer_parameters:  _CppOptimizerParameters
    :param hyperparameter_domain: [lower, upper] bound pairs for each hyperparameter dimension in LOG-10 SPACE
    :type hyperparameter_domain: list of float64 with shape (num_hyperparameters, 2)
    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: optimized hyperparameters
    :rtype: list of float64 with shape (num_hyperparameters, )
    )%%");

  boost::python::def("evaluate_log_likelihood_at_hyperparameter_list", EvaluateLogLikelihoodAtHyperparameterListWrapper, R"%%(
    Evaluates the specified log likelihood measure of model fit at each member of
    hyperparameter_list. Useful for plotting.

    Equivalent to::

      result = []
      for hyperparameters in hyperparameter_list:
          result.append(compute_log_likelihood(hyperparameters, ...))

    But this method is substantially faster (loops in C++ and is multithreaded).

    ``n_hyper`` denotes the number of hyperparameters

    :param hyperparameter_list[num_multistarts][n_hyper]: list of hyperparameters at which to evaluate log likelihood
    :type hyperparameter_list: list of float64 with shape (num_multistarts, num_hyperparameters)
    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param objective_mode: describes which log likelihood measure to compute (e.g., kLogMarginalLikelihood)
    :type objective_mode: GPP.LogLikelihoodTypes (enum)
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :param num_multistarts: number hyperparameter sets at which to compute log likelihood
    :type num_multistarts: int > 0
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: log likelihood values at each point of the hyperparameter_list list, in the same order
    :rtype: list of float64 with shape (num_multistarts, )
    )%%");
}

}  // end namespace optimal_learning