#pragma once

#include <OpenEXR/ImathVec.h>
#include <map>

#include <ai.h>

#include <openvdb/openvdb.h>
#include <openvdb/tools/Interpolation.h>
#include <openvdb/tools/RayIntersector.h>

#include <nanovdb/NanoVDB.h>
#include <nanovdb/util/IO.h>
#include <nanovdb/util/GridHandle.h>

#include <boost/thread/tss.hpp>

// some useful shorthands
typedef openvdb::FloatGrid FloatGridType;
typedef FloatGridType::TreeType FloatGridTreeType;

typedef openvdb::Vec3SGrid Vec3SGridType;
typedef Vec3SGridType::TreeType Vec3SGridTreeType;

/// Abstract base class for cache instances. One CacheInstance represents a single layer from a Field3D or OpenVDB cache
class CacheInstance
{
public:
	enum CacheType
	{
		kField3D = 0,
		kOpenVDB
	};
	enum Interpolation
	{
		kClosest = 0,
		kTriliear,
		kTricubic
	};
	CacheInstance();
	virtual ~CacheInstance();

	/// Lookup an interpolated value from the cache
	/// @param wsP The world-space position to look up
	/// @return The field value at @wsP
	
	//virtual void* lookup(const Imath::V3d& wsP) const = 0;
	
	/// Get the world-space bounding-box of the field
	/// @param Bounding box minimum
	/// @param Bounding box maximum
	virtual void bounds(Imath::V3d& bbmin, Imath::V3d& bbmax) const = 0;
	/// Is the cache valid or not?
	inline bool valid() const { return m_Valid; }
	/// What type is the cache (either kField3D or kOpenVDB)
	inline CacheType cacheType() const { return m_CacheType; }
	/// How much memory is this CacheInstance using?
	virtual size_t memUsage() const = 0;
	/// How big are the voxels?
	virtual Imath::V3d voxelSize() const = 0;

	typedef boost::shared_ptr<CacheInstance> Ptr;

protected:
	bool m_Valid;
	CacheType m_CacheType;
};



/// The VolumeCache object manages loading particular layers from cache files and managing their lifetime in memory.
/// To use it callers should use VolumeCache::instance() to get the singleton instance, then call readFloatField() to read a layer from a cache
/// into memory. This returns a Handle object that can be used to get the actual CacheInstance object that stores the layer. If the returned
/// handle is less than 0 then something went wrong loading the layer. Attempting to get a CacheInstance from such a Handle will return a null 
/// pointer.
class VolumeCache
{
public:
	/// Handle for accessing loaded volume cache data
	typedef int Handle;
	static const int kInvalidFile = -1;
	static const int kInvalidField = -2;
	static const int kInvalidFieldType = -3;
	static const int kUnknownError = -9;

	enum FilterType
	{
		kFast = 0,
		kLinear,
		kSmooth
	};

	/// Get the volume cache manager singleton
	static VolumeCache* instance();

	/// Read a float field/grid from the given volume file
	/// @return A handle for accessing the grid data. If the read failed the returned Handle will have value < 0
	/// @param cacheName Path to the Field3D or OpenVDB file to read
	/// @param fieldName Name of the field in the cache to read
	Handle readField(const std::string& cacheName, const std::string& fieldName, bool openToNanoVDB);

	/// Evict a loaded field from memory. The object will be destroyed after any remaining Ptrs held by the caller are destroyed
	void evict(Handle h);

	/// Report the total size, in bytes, of the memory used by caches currently loaded
	size_t memUsage() const;

	/// Return the CacheInstance for the given Handle
	CacheInstance::Ptr getCacheInstance(Handle h);
	bool m_Initialized = false;

private:
	VolumeCache() : m_NumEntries(0) {}
	static VolumeCache* m_Instance;

	struct StaticInit
	{
	public:
		StaticInit();
	};
	static StaticInit m_StaticInit;

	typedef std::map<std::string, Handle> FileHandleMap;
	FileHandleMap m_FileHandleMap;

	typedef std::map<Handle, CacheInstance::Ptr> CacheInstanceMap;
	CacheInstanceMap m_CacheInstanceMap;

	// load a grid from an OpenVDB file and create a new CacheInstance for it
	Handle readOpenVDBGrid(const std::string& filename, const std::string& gridName, bool openToNanoVDB);
	Handle readNanoVDBGrid(const std::string& filename, const std::string& gridName, bool openToOpenVDB);

	//Handle readField3DField(const std::string& filename, const std::string& fieldName);
	//Handle readBiforstField(const std::string& filename, const std::string& channelName);
	//Handle readAuraField(const std::string& filename, const std::string& channelName);

	uint32_t m_NumEntries;

	friend class VolumeCache::StaticInit;
};

/// OpenVDB CacheInstance implementation
class OpenVDBCacheInstance: public CacheInstance
{
public:
	OpenVDBCacheInstance(VolumeCache::Handle h, openvdb::GridBase::Ptr grid);

	template <typename GridType>
	auto sample(int interplotion, const Imath::V3d& wsP) const -> typename GridType::ValueType;

	template <typename Intersector>
	void march_extents(const AtVolumeIntersectionInfo* info, const openvdb::math::Ray<openvdb::Real>& ray, const Intersector* master_intersector);

	void bounds(Imath::V3d& bbmin, Imath::V3d& bbmax) const;

	size_t memUsage() const;

	Imath::V3d voxelSize() const;

	openvdb::GridBase::Ptr m_Grid;
	VolumeCache::Handle m_Handle;

	typedef boost::shared_ptr<OpenVDBCacheInstance> Ptr;
};

class NanoVDBCacheInstance : public CacheInstance
{
public:
	NanoVDBCacheInstance(VolumeCache::Handle, nanovdb::GridHandle<> gridHandle);
	
	template <typename GridType>
	auto sample(int interplotion, const Imath::V3d& wsP) const -> typename GridType;

	bool march_extents(const AtVolumeIntersectionInfo* info, const Imath::V3f &ray_original, const Imath::V3f &ray_direction, float t0, float t1);

	void bounds(Imath::V3d& bbmin, Imath::V3d& bbmax) const;
	size_t memUsage() const;

	Imath::V3d voxelSize() const;

	nanovdb::GridHandle<>* m_GridHandle;
	VolumeCache::Handle m_Handle;

	typedef boost::shared_ptr<NanoVDBCacheInstance> Ptr;
};
