#include <cassert>
#include <sstream>

#include "UnitTest++.h"
#include "test_utils.hh"

#include "geners/Record.hh"
#include "geners/Reference.hh"
#include "geners/StringArchive.hh"
#include "geners/BinaryFileArchive.hh"
#include "geners/MultiFileArchive.hh"
#include "geners/arrayIO.hh"

#define DIM 5
#define NPOINTS 50

using namespace gs;
using namespace std;

namespace {
    class Persistent2
    {
    public:
        inline Persistent2(int n=0) : n_(n) {}

        inline bool operator==(const Persistent2& r) const
            {return n_ == r.n_;}
        inline bool operator!=(const Persistent2& r) const
            {return !(*this == r);}

        inline Persistent2& operator+=(const int& r)
        {
            n_ += r;
            return *this;
        }

        inline ClassId classId() const {return ClassId(*this);}

        inline bool write(std::ostream& os) const
        {
            write_pod(os, n_);
            return !os.bad() && !os.fail();
        }

        static inline const char* classname() {return "gs::test::Persistent2";}
        static inline unsigned version() {return 1;}
        static inline Persistent2* read(const ClassId& id, std::istream& in)
        {
            assert(id == ClassId::makeId<Persistent2>());
            int n;
            read_pod(in, &n);
            if (in.fail())
                throw gs::IOReadFailure("In Persistent2::read: "
                                        "input stream failure");
            else
                return new Persistent2(n);
        }

    private:
        int n_;
    };

    void test_archive(AbsArchive& ar, const bool readStuff=true)
    {
        typedef CPP11_array<Persistent2,DIM> Point;
        typedef CPP11_array<double,DIM> Point2;

        CHECK(ar.isOpen());
        if (readStuff)
            CHECK(ar.isReadable());
        CHECK(ar.isWritable());

        // Create a table of objects
        std::vector<Point> data;
        Point location;
        for (unsigned i=0; i<NPOINTS; ++i)
        {
            for (unsigned idim=0; idim<DIM; ++idim)
                location[idim] = Persistent2(
                    static_cast<int>(test_rng()*1000000));
            data.push_back(location);
        }
        CHECK(data[0] != data[1] || data[0] != data[2]);

        // Write out the table of objects
        ar << Record(data, "name1", "category1");

        for (unsigned i=0; i<NPOINTS; ++i)
        {
            for (unsigned idim=0; idim<DIM; ++idim)
                location[idim] = Persistent2(
                    static_cast<int>(test_rng()*1000000));
            data.push_back(location);
        }
        ArchiveRecord<std::vector<Point> > rto(
            Record(data, "name1", "category1"));
        ar << rto;

        // Create a table of PODs
        std::vector<Point2> data2;
        Point2 datum;
        for (unsigned i=0; i<NPOINTS; ++i)
        {
            for (unsigned idim=0; idim<DIM; ++idim)
                datum[idim] = test_rng();
            data2.push_back(datum);
        }

        // Write out the table of PODs
        ArchiveRecord<std::vector<Point2> > rtp(
            Record(data2, "name2", "category2"));
        ar << rtp;

        // Duplicate some records in another archive
        StringArchive dup;
        if (readStuff && ar.isReadable())
        {
            ar.copyItem(rto.id(), &dup, "hren", "blin");
            ar.copyItem(rtp.id(), &dup);
        }

        // Write out a POD
        unsigned mynumber = 12345;
        ar << Record(mynumber, "my number", "who cares");

        // Write out an archive
        StringArchive sa;
        sa << Record(mynumber, "clap", "dup");
        ar << Record(sa, "my archive", "yok");

        if (readStuff)
        {
            const std::vector<std::string>& cats = ar.allCategories();
            CHECK_EQUAL(4U, cats.size());
        }

        // Read back the archive
        if (readStuff)
        {
            Reference<StringArchive> ref(ar, "my archive", "yok");
            CHECK(ref.unique());
            CPP11_auto_ptr<StringArchive> psa = ref.get(0);
            CHECK(sa == *psa);

            Reference<unsigned> ref4(*psa, "clap", "dup");
            unsigned dummy = 0;
            ref4.restore(0, &dummy);
            CHECK_EQUAL(mynumber, dummy);
        }

        // Read back the table of PODs and check for identity
        // with what was written
        if (readStuff)
        {
            std::vector<Point2> read2;
            Reference<std::vector<Point2> >(
                ar, "name2", "category2").restore(0, &read2);
            CHECK(data2 == read2);

            if (ar.isReadable())
            {
                std::vector<Point2> read3;
                Reference<std::vector<Point2> >(
                    dup, "name2", "category2").restore(0, &read3);
                CHECK(data2 == read3);
            }
        }

        // Read back a pod
        if (readStuff)
        {
            Reference<unsigned> ref4(ar, "my number", "who cares");
            unsigned dummy = 0;
            ref4.restore(0, &dummy);
            CHECK_EQUAL(mynumber, dummy);

            CPP11_auto_ptr<unsigned> uns2 = Reference<unsigned>(
                ar, "my number", "who cares").get(0);
            CHECK_EQUAL(mynumber, *uns2);
        }

        // Write out a vector of PODs
        std::vector<double> vecd;
        for (unsigned i=0; i<NPOINTS; ++i)
            vecd.push_back(test_rng());
        ar << Record(vecd, "my vector", 0);

        // Write out a vector of objects
        std::vector<Persistent2> veco;
        for (unsigned i=0; i<NPOINTS; ++i)
            veco.push_back(Persistent2(static_cast<int>(test_rng()*1000000)));
        ar << Record(veco, "my vector 2", "duh");

        // Write out a copy of a string
        ar << ValueRecord(std::string("message 1"), "some message", "uh");

        // Read back and check for identity with what was written
        if (readStuff)
        {
            Reference<std::vector<Point> > ref(ar, "name1", "category1");
            CHECK(ref.size() == 2);
            std::vector<Point> read;
            ref.restore(0, &read);
            CHECK(data != read);
            ref.restore(1, &read);
            CHECK(data == read);
            CPP11_auto_ptr<std::vector<Point> > tmp = ref.get(1);
            CHECK(data == *tmp);

            if (ar.isReadable())
            {
                Reference<std::vector<Point> > copyref(dup, "hren", "blin");
                CHECK(copyref.unique());
                CPP11_auto_ptr<std::vector<Point> > copytmp = copyref.get(0);
                CHECK(data == *copytmp);
            }

            // Read back the vector of PODs
            std::vector<double> haha;
            Reference<std::vector<double> >(ar, "my vector", 0).restore(
                0, &haha);
            CHECK(haha == vecd);

            // Read back the vector of objects
            CPP11_auto_ptr<std::vector<Persistent2> > rv = 
                Reference<std::vector<Persistent2> >(
                    ar, "my vector 2", "duh").get(0);
            CHECK(*rv == veco);

            // Read back the string message
            Reference<std::string> sref(ar, "some message", "uh");
            std::string rb;
            sref.restore(0, &rb);
            CHECK(rb == std::string("message 1"));
        }
    }

    TEST(ArchiveIO_string)
    {
        StringArchive ar("Test Archive");
        test_archive(ar);

        const std::string& st(ar.str());
        CHECK_EQUAL(st.size(), ar.dataSize());

        std::ostringstream os;
        write_item(os, ar);

        std::istringstream is(os.str());
        ClassId myid(is, 1);
        CPP11_auto_ptr<StringArchive> ar2(StringArchive::read(myid, is));
        CHECK(ar2.get());
        CHECK(ar == *ar2);
    }

    TEST(ArchiveIO_binary)
    {
        BinaryFileArchive ar("archive", "w+:cat=i", "binary file archive test");
        test_archive(ar);
        ar.flush();

        BinaryFileArchive ar2("archive", "r");
        const unsigned long long nItems = ar.size();
        CHECK_EQUAL(nItems, ar2.size());
        const unsigned long long offset = 1ULL;
        for (unsigned long long i=offset; i<nItems+offset; ++i)
            CHECK(*ar.catalogEntry(i) == *ar2.catalogEntry(i));

        BinaryFileArchive ar3("archive", "w:cat=i",
                              "binary file archive test");
        test_archive(ar3, false);
        ar3.flush();

        BinaryFileArchive ar4("archive", "r");
        CHECK_EQUAL(nItems, ar4.size());
        for (unsigned long long i=offset; i<nItems+offset; ++i)
            CHECK(*ar.catalogEntry(i) == *ar4.catalogEntry(i));
    }

    TEST(ArchiveIO_multifile)
    {
        MultiFileArchive ar("archive_m", "w+:cat=i",
                            "multifile archive test", 0);
        test_archive(ar);
        ar.flush();

        MultiFileArchive ar2("archive_m", "r");
        const unsigned long long nItems = ar.size();
        CHECK_EQUAL(nItems, ar2.size());
        const unsigned long long offset = 1ULL;
        for (unsigned long long i=offset; i<nItems+offset; ++i)
            CHECK(*ar.catalogEntry(i) == *ar2.catalogEntry(i));

        MultiFileArchive ar3("archive_m", "w:cat=i",
                             "multifile archive test", 0);
        test_archive(ar3, false);
        ar3.flush();

        MultiFileArchive ar4("archive_m", "r");
        CHECK_EQUAL(nItems, ar4.size());
        for (unsigned long long i=offset; i<nItems+offset; ++i)
            CHECK(*ar.catalogEntry(i) == *ar4.catalogEntry(i));
    }

    TEST(ArchiveIO_compression_bzip2)
    {
        BinaryFileArchive ar("archive_bzip2", "w+:z=b",
                             "bzip2 compressed archive");
        test_archive(ar);
        ar.flush();
    }

    TEST(ArchiveIO_compression_zlib)
    {
        BinaryFileArchive ar("archive_zlib", "w+:z=z",
                             "zlib compressed archive");
        test_archive(ar);
        ar.flush();
    }

    TEST(ArchiveIO_compression_options)
    {
        BinaryFileArchive ar("archive_zlib", "w+:z=z:cl=9:cb=4096:cm=2048",
                             "zlib compression options");
        test_archive(ar);
        ar.flush();

        CHECK(ar.compressionMode() == CStringStream::ZLIB);
        CHECK_EQUAL(4096U, ar.compressionBufferSize());
        CHECK_EQUAL(2048U, ar.minSizeToCompress());
        CHECK_EQUAL(9, ar.compressionLevel());
    }

    TEST(ArchiveIO_readwrite)
    {
        {
            BinaryFileArchive ar("archive_add", "w");
            std::string dummy = "dummy";
            ar << Record(dummy, "A string", "some");
        }
        {
            BinaryFileArchive ar("archive_add", "r+");
            std::string dummy = "dummy2";
            ar << Record(dummy, "Another string", "category");
        }
    }
}
