/*
   Copyright (c) 2019 by The ThreadDB Project
   All Rights Reserved.

   ThreadDB undergoes the BSD License 2.0. You should have received a copy along with this program; if not, write to the ThreadDB Project.
   To obtain a full unlimited version contact thethreaddbproject(at)gmail.com.

   rgridsample.cpp - Sample for 2D range queries.
*/

#include <threaddbRGrid.h>
#include <threaddbStream.h>

#include <fstream>
#include <ios>
#include <random>
#include <iostream>
#include <list>
#include <sstream>
#include <map>

#if defined(_WIN32)
    #define PATH_SEP "\\"
#else
    #define PATH_SEP "/"
#endif

typedef int64_t coordinate;

template<class T>
class Shape1D
{
public:
    typedef T value_type;
    typedef tdb::Point<T, 1> Point;
    typedef tdb::Box<T, 1> Box;

    const Box& bounding_box() const { return m_Box; }
    Box& bounding_box() { return m_Box; }

    Shape1D() :
        m_Box(Point(std::numeric_limits<T>::max()), Point(std::numeric_limits<T>::min()))
    {
    }

    Shape1D(tdb::database& rDb_p, tdb::ReadInfo& rReadInfo_p) :
        m_Box()
    {
        rDb_p.Recover(sizeof(m_Box), (char*)&m_Box, rReadInfo_p);
    }

    Shape1D(const Box& rBox_p) :
        m_Box(rBox_p)
    {
    }

    size_t size() const { return sizeof(*this); }
    const char* c_ptr() const { return (const char*)this; }

private:
    Box m_Box;
};

template<class T>
class Shape2D
{
public:
    typedef T value_type;
    typedef tdb::Point<T, 2> Point;
    typedef tdb::Box<T, 2> Box;

    const Box& bounding_box() const { return m_Box; }
    Box& bounding_box() { return m_Box; }

    Shape2D() :
        m_Size(sizeof(*this)),
        m_Box(Point(std::numeric_limits<T>::max(), std::numeric_limits<T>::max()),
            Point(std::numeric_limits<T>::min(), std::numeric_limits<T>::min()))
    {
    }

    Shape2D(tdb::database& rDb_p, tdb::ReadInfo& rReadInfo_p) :
        m_Size(),
        m_Box()
    {
        rDb_p.Recover(sizeof(m_Size), (char*)&m_Size, rReadInfo_p);
        rDb_p.Recover(sizeof(m_Box), (char*)&m_Box, rReadInfo_p);
    }

    Shape2D(const Box& rBox_p) :
        m_Size(sizeof(Shape2D)),
        m_Box(rBox_p)
    {
    }

    size_t size() const { return m_Size; }
    const char* c_ptr() const { return (const char*)this; }

private:
    uint8_t m_Size;
    Box m_Box;
};

template<class T>
class Shape3D
{
public:
    typedef T value_type;
    typedef tdb::Point<T, 3> Point;
    typedef tdb::Box<T, 3> Box;

    const Box& bounding_box() const { return m_Box; }
    Box& bounding_box() { return m_Box; }

    Shape3D() :
        m_Box(Point(std::numeric_limits<T>::max(), std::numeric_limits<T>::max(), std::numeric_limits<T>::max()),
            Point(std::numeric_limits<T>::min(), std::numeric_limits<T>::min(), std::numeric_limits<T>::min()))
    {
    }

    Shape3D(tdb::database& rDb_p, tdb::ReadInfo& rReadInfo_p) :
        m_Box()
    {
        rDb_p.Recover(sizeof(m_Box), (char*)&m_Box, rReadInfo_p);
    }

    Shape3D(const Box& rBox_p) :
        m_Box(rBox_p)
    {
    }

    size_t size() const { return sizeof(*this); }
    const char* c_ptr() const { return (const char*)this; }

private:
    Box m_Box;
};

template<class T>
class RandomGenerator
{
public:
    RandomGenerator(int Seed_p) :
        m_MT19937(Seed_p)
    {
    }

    T Rand(T Min_p, T Max_p)
    {
        std::uniform_int_distribution<T> distribution(Min_p, Max_p);
        return distribution(m_MT19937);
    }

private:
    std::mt19937 m_MT19937;
};

typedef tdb::Point<coordinate, 1> Point1D;
typedef tdb::Box<coordinate, 1> Box1D;
typedef Shape1D<coordinate> Shape1D_;

typedef tdb::Point<coordinate, 2> Point2D;
typedef tdb::Box<coordinate, 2> Box2D;
typedef Shape2D<coordinate> Shape2D_;

typedef tdb::Point<coordinate, 3> Point3D;
typedef tdb::Box<coordinate, 3> Box3D;
typedef Shape3D<coordinate> Shape3D_;

// [Example5.5 example]
template<class T>
struct Counter : public tdb::Functor<T>
{
    Counter() : m_Cnt(0) {}
    void operator() (const T& rShape_p)
    {
        ++m_Cnt;
    }
    std::atomic<int64_t> m_Cnt;
};
// [Example5.5 example]

const coordinate shapeDimension = 1000;
const coordinate scenarioDimension = 2000000;

void threadStore(tdb::rgrid<Shape3D_, coordinate, 3>* pRgrid_p, RandomGenerator<coordinate>* pRand_p)
{
    tdb::rgrid<Shape3D_, coordinate, 3>& rgrid(*pRgrid_p);
    RandomGenerator<coordinate>& randomGenerator(*pRand_p);

    Shape3D_ Shape3D;
    Box3D& rBox(Shape3D.bounding_box());

    size_t numberOfGeneratedShapes = 25000;

    for (size_t idx(0); idx < numberOfGeneratedShapes; ++idx)
    {
        coordinate l = randomGenerator.Rand(1, shapeDimension);
        coordinate w = randomGenerator.Rand(1, shapeDimension);
        coordinate h = randomGenerator.Rand(1, shapeDimension);

        rBox.max_corner() = rBox.min_corner() + Point3D(l, w, h);

        if ((idx > 0) && (idx % 10000 == 0))
        {
            std::ostringstream os;
            os << "Inserted: " << idx << " Shapes" << std::endl;
            std::cout << os.str(); std::cout.flush();
        }

        coordinate x = randomGenerator.Rand(0, scenarioDimension - l);
        coordinate y = randomGenerator.Rand(0, scenarioDimension - w);
        coordinate z = randomGenerator.Rand(0, scenarioDimension - h);
        rBox += (Point3D(x, y, z) - rBox.min_corner());

        rgrid.add(rBox, Shape3D);
    }
};

int main(int argc, char* argv[])
{
    std::string tmpFolder;

    if (argc < 2)
    {
        std::cout << "The testrun of threadDB requires a folder name to store the temporary database files" << std::endl;
        exit(1);
    }

    tmpFolder = argv[1];
    tmpFolder.erase(tmpFolder.find_last_not_of("\\/") + 1);

    tdb::database database(1024);

    std::list< std::string > createdFiles;

    std::cout << "Creating threads" << std::endl;
    createdFiles.push_back(database.NewThread(tmpFolder.c_str()));
    createdFiles.push_back(database.NewThread(tmpFolder.c_str()));
    createdFiles.push_back(database.NewThread(tmpFolder.c_str()));
    createdFiles.push_back(database.NewThread(tmpFolder.c_str()));

    RandomGenerator<coordinate> randomGenerator(1);

    {
        const size_t numberOfGeneratedShapes = 1000000;

        Shape1D_ Shape1D;
        Box1D& rBox(Shape1D.bounding_box());

        const Box1D scenarioWindow(Point1D(0), Point1D(scenarioDimension));

        const std::string tempFile = tmpFolder + std::string(PATH_SEP) + "1DGrid.tmp";
        const uint64_t packageID = database.NewPackage();

        {
            // [Example5.1 example]
            tdb::rgrid<Shape1D_, coordinate, 1> rgrid(scenarioWindow, 10, database);
            // [Example5.1 example]

            std::cout << "Start filling 1D" << std::endl;
            for (size_t idx(0); idx < numberOfGeneratedShapes; ++idx)
            {
                coordinate w = randomGenerator.Rand(1, shapeDimension);
                rBox.max_corner() = rBox.min_corner() + Point1D(w);

                if (idx % 10000 == 0)
                    std::cout << "Inserted: " << idx << " Shapes" << std::endl;

                coordinate x = randomGenerator.Rand(0, scenarioDimension - w);
                rBox += (Point1D(x) - rBox.min_corner());

                // [Example5.2 example]
                rgrid.add(rBox, Shape1D);
                // [Example5.2 example]
            }

            database.Synchronize();

            // [Example5.3 example]
            tdb::ostream os(packageID, database);
            os << rgrid;
            os.flush();
            // [Example5.3 example]

            // [Example5.4 example]
            tdb::istream is(packageID, database);

            tdb::rgrid<Shape1D_, coordinate, 1> rgrid_(is, database);
            // [Example5.4 example]

            {
                const size_t numberOfQueries = 100;

                std::cout << "Start querying 1D" << std::endl;
                for (size_t idx(0); idx < numberOfQueries; ++idx)
                {
                    // [Example5.6 example]
                    Counter<Shape1D_> shapeCount;
                    Counter<Shape1D_> shapeCount_;
                    // [Example5.6 example]

                    coordinate w = randomGenerator.Rand(1, scenarioDimension);
                    rBox.max_corner() = rBox.min_corner() + Point1D(w);

                    coordinate x = randomGenerator.Rand(0, scenarioDimension - w);
                    rBox += (Point1D(x) - rBox.min_corner());

                    // [Example5.7 example]
                    rgrid.query(rBox, shapeCount, 1);
                    rgrid_.query(rBox, shapeCount_, 3);
                    // [Example5.7 example]

                    if (shapeCount.m_Cnt != shapeCount_.m_Cnt)
                    {
                        throw std::runtime_error("Incosisten query results");
                    }

                    std::cout << "1D-Shapes (1st): " << shapeCount.m_Cnt << std::endl;
                    std::cout << "1D-Shapes (2nd): " << shapeCount.m_Cnt << std::endl;
                }
            }
        }
    }

    {
        const size_t numberOfGeneratedShapes = 1000000;

        Shape2D_ Shape2D;
        Box2D& rBox(Shape2D.bounding_box());

        const std::string tempFile = tmpFolder + std::string(PATH_SEP) + "2DGrid.tmp";
        {
            const Box2D scenarioWindow(Point2D(0, 0), Point2D(scenarioDimension, scenarioDimension));

            tdb::rgrid<Shape2D_, coordinate, 2> rgrid(scenarioWindow, 5, database);

            std::cout << "Start filling 2D" << std::endl;
            for (size_t idx(0); idx < numberOfGeneratedShapes; ++idx)
            {
                coordinate w = randomGenerator.Rand(1, shapeDimension);
                coordinate h = randomGenerator.Rand(1, shapeDimension);
                rBox.max_corner() = rBox.min_corner() + Point2D(w, h);

                if (idx % 10000 == 0)
                    std::cout << "Inserted: " << idx << " Shapes" << std::endl;

                coordinate x = randomGenerator.Rand(0, scenarioDimension - w);
                coordinate y = randomGenerator.Rand(0, scenarioDimension - h);
                rBox += (Point2D(x, y) - rBox.min_corner());

                rgrid.add(rBox, Shape2D);
            }

            database.Synchronize();

            std::ofstream ofs;
            ofs.open(tempFile.c_str(), std::ios::out | std::ios::binary);
            if (!ofs.is_open())
            {
                throw std::runtime_error("Unable to open file" + tempFile);
            }
            createdFiles.push_back(tempFile);

            ofs << rgrid;
        }

        std::ifstream ifs;
        ifs.open(tempFile.c_str(), std::ios::in | std::ios::binary);
        if (!ifs.is_open())
        {
            throw std::runtime_error("Unable to open file" + tempFile);
        }

        tdb::rgrid<Shape2D_, coordinate, 2> rgrid(ifs, database);

        std::cout << "Continue filling 2D" << std::endl;
        for (size_t idx(0); idx < numberOfGeneratedShapes; ++idx)
        {
            coordinate w = randomGenerator.Rand(1, shapeDimension);
            coordinate h = randomGenerator.Rand(1, shapeDimension);
            rBox.max_corner() = rBox.min_corner() + Point2D(w, h);

            if (idx % 10000 == 0)
                std::cout << "Inserted: " << idx << " Shapes" << std::endl;

            coordinate x = randomGenerator.Rand(0, scenarioDimension - w);
            coordinate y = randomGenerator.Rand(0, scenarioDimension - h);
            rBox += (Point2D(x, y) - rBox.min_corner());

            rgrid.add(rBox, Shape2D);
        }

        const size_t numberOfQueries = 100;

        std::cout << "Start querying 2D" << std::endl;
        for (size_t idx(0); idx < numberOfQueries; ++idx)
        {
            Counter<Shape2D_> shapeCount;

            coordinate w = randomGenerator.Rand(1, scenarioDimension);
            coordinate h = randomGenerator.Rand(1, scenarioDimension);
            rBox.max_corner() = rBox.min_corner() + Point2D(w, h);

            coordinate x = randomGenerator.Rand(0, scenarioDimension - w);
            coordinate y = randomGenerator.Rand(0, scenarioDimension - h);
            rBox += (Point2D(x, y) - rBox.min_corner());

            rgrid.query(rBox, shapeCount, 4);

            std::cout << "2D-Shapes: " << shapeCount.m_Cnt << std::endl;
        }
    }

    {
        const Box3D scenarioWindow(Point3D(0, 0, 0), Point3D(scenarioDimension, scenarioDimension, scenarioDimension));

        Shape3D_ Shape3D;
        Box3D& rBox(Shape3D.bounding_box());

        const std::string tempFile = tmpFolder + std::string(PATH_SEP) + "3DGrid.tmp";
        {
            tdb::rgrid<Shape3D_, coordinate, 3> rgrid(scenarioWindow, 4, database);

            std::cout << "Start filling 3D" << std::endl; std::cout.flush();

            std::list< std::shared_ptr<std::thread> > threadGroup;
            threadGroup.push_back(std::shared_ptr<std::thread>(new std::thread(&threadStore, &rgrid, &randomGenerator)));
            threadGroup.push_back(std::shared_ptr<std::thread>(new std::thread(&threadStore, &rgrid, &randomGenerator)));
            threadGroup.push_back(std::shared_ptr<std::thread>(new std::thread(&threadStore, &rgrid, &randomGenerator)));
            threadGroup.push_back(std::shared_ptr<std::thread>(new std::thread(&threadStore, &rgrid, &randomGenerator)));

            for (auto iter(threadGroup.begin()); iter != threadGroup.end(); ++iter)
                (*iter)->join();

            database.Synchronize();

            std::ofstream ofs;
            ofs.open(tempFile.c_str(), std::ios::out | std::ios::binary);
            if (!ofs.is_open())
            {
                throw std::runtime_error("Unable to open file" + tempFile);
            }
            createdFiles.push_back(tempFile);

            ofs << rgrid;
        }

        std::ifstream ifs;
        ifs.open(tempFile.c_str(), std::ios::in | std::ios::binary);
        if (!ifs.is_open())
        {
            throw std::runtime_error("Unable to open file" + tempFile);
        }

        tdb::rgrid<Shape3D_, coordinate, 3> rgrid(ifs, database);

        const size_t numberOfQueries = 100;

        std::cout << "Start querying 3D" << std::endl;
        for (size_t idx(0); idx < numberOfQueries; ++idx)
        {
            Counter<Shape3D_> shapeCount;

            coordinate l = randomGenerator.Rand(1, scenarioDimension);
            coordinate w = randomGenerator.Rand(1, scenarioDimension);
            coordinate h = randomGenerator.Rand(1, scenarioDimension);
            rBox.max_corner() = rBox.min_corner() + Point3D(l, w, h);

            coordinate x = randomGenerator.Rand(0, scenarioDimension - l);
            coordinate y = randomGenerator.Rand(0, scenarioDimension - w);
            coordinate z = randomGenerator.Rand(0, scenarioDimension - h);
            rBox += (Point3D(x, y, z) - rBox.min_corner());

            rgrid.query(rBox, shapeCount, 4);

            std::cout << "3D-Shapes: " << shapeCount.m_Cnt << std::endl;
        }
    }

    std::cout << "Remove temporary files" << std::endl;

    for (std::list<std::string>::const_iterator iter(createdFiles.begin()); iter != createdFiles.end(); ++iter)
    {
        try
        {
            std::remove((*iter).c_str());
        }
        catch (...)
        {
            ;
        }
    }
}
